Writing your first Stacks contract in StxScript
This is a walkthrough for someone who has written modern typed code — TypeScript, Rust, Swift — and wants to ship their first Stacks smart contract this afternoon. We will install StxScript, scaffold a token project, change one thing, transpile, and read the resulting Clarity together. By the end you will know what the moving parts are and what each one does.
The example follows the README’s quick start verbatim, with a few extra explanations along the way.
Step 0: what you are about to build
Stacks is a Bitcoin layer-2 with a smart contract language called Clarity. Clarity is intentionally restrictive — decidable execution, no recursion, predictable gas — and that makes it safe but a little unfamiliar if you come from mainstream typed languages. StxScript is a source language that compiles to Clarity. You write TypeScript-flavored code, run a build, and end up with a .clar file that deploys to Stacks like any hand-written Clarity contract.
We will scaffold a token contract — a minimal fungible token — because it is the smallest interesting smart contract.
Step 1: install
StxScript ships as a Python 3.10+ package. You probably have pip already.
pip install stxscript
That command installs the CLI binary, the formatter, the linter, the language server (stxscript-lsp), and the project scaffolder. Verify it worked:
stxscript --version
You should see 0.3.0 or later. If pip complains about Python version, install Python 3.10 or newer. On macOS, brew install python@3.11 is the path of least resistance.
Step 2: scaffold the project
The README spells out the templates: basic, token, nft, defi. We want token.
stxscript new my-token --template token
cd my-token
The scaffold lays out source, tests, and project metadata. The exact tree depends on the template version, but you should see at least:
- a
src/directory with one or more.stxfiles - a
tests/directory with example tests - a project configuration file (the
stxscript pkgsystem reads this)
Open the main .stx file in your editor. If you use VS Code, install the StxScript extension first — syntax highlighting and inline diagnostics make the rest of this walkthrough easier.
Step 3: read the source
The starter token contract follows the same shape as the README example. Constants for the token name and supply, a total_supply variable, a balances map, and a few public functions. Something like:
const TOKEN_NAME: string = "MyToken";
const MAX_SUPPLY: uint = 1000000u;
let total_supply: uint = 0u;
map balances<principal, uint>;
@public
function mint(amount: uint): Response<bool, uint> {
if (total_supply + amount > MAX_SUPPLY) {
return err(1u);
}
total_supply = total_supply + amount;
return ok(true);
}
@readonly
function get_balance(account: principal): uint {
match balances.get(account) {
some(balance) => balance,
none => 0u
}
}
This is the example from the project README. Read it like TypeScript with a few new shapes:
constandletwork like in TypeScript, but every variable has an explicit type andumarks unsigned literals.map balances<principal, uint>;declares a key/value map. There is nonew Map()— this is a contract-level storage declaration.@publicmarks a function as callable from outside the contract.@readonlymarks it as side-effect-free. Plainfunction(no decorator) is private.Response<bool, uint>is the error monad. Eitherok(value)orerr(code). Public functions must return a Response.match balances.get(account) { ... }destructures anOptional<uint>— the result of looking up a missing key isnone.
If you have written Rust or Swift, every shape here has a near-identical analog. If you have only written TypeScript, the new ideas are Response (think Result<T, E> from Rust) and Optional (T | undefined made type-safe).
Step 4: change one thing
Let us add a hard cap per account. We want a single principal to never hold more than 100,000 tokens.
Edit the mint function:
const PER_ACCOUNT_CAP: uint = 100000u;
@public
function mint(amount: uint): Response<bool, uint> {
if (total_supply + amount > MAX_SUPPLY) {
return err(1u);
}
let current: uint = get_balance(tx-sender);
if (current + amount > PER_ACCOUNT_CAP) {
return err(2u);
}
let new_balance: uint = current + amount;
balances.set(tx-sender, new_balance);
total_supply = total_supply + amount;
return ok(true);
}
Three things to notice while typing this:
tx-senderis a Clarity primitive, used as-is in StxScript. It is the principal of whoever called the function. No imports needed.balances.set(key, value)writes to the map. The transpiler will turn it into(map-set balances key value).let current: uint = get_balance(tx-sender);calls our own read-only function. The transpiler inlines the call into the Clarity output as(get-balance tx-sender)— note the kebab-case rewrite of function names.
Step 5: check your work before building
Before transpiling, run the static checks. They are fast and they catch the same class of bugs Clarity would catch at deploy time.
stxscript check src/my-token.stx
stxscript lint src/my-token.stx
check does syntax. lint does static analysis — unused variables, missing returns, trait compliance mismatches. If either flags something, fix it before transpiling. The point of the language is to push deploy-time errors into editor-time errors.
Step 6: transpile
stxscript build src/my-token.stx build/my-token.clar
That writes a build/my-token.clar file containing the generated Clarity. Open it.
The output for the mint function above should look roughly like:
(define-public (mint (amount uint))
(if (> (+ (var-get total_supply) amount) MAX_SUPPLY)
(err u1)
(let ((current (get-balance tx-sender)))
(if (> (+ current amount) PER_ACCOUNT_CAP)
(err u2)
(begin
(map-set balances tx-sender (+ current amount))
(var-set total_supply (+ (var-get total_supply) amount))
(ok true))))))
Read it next to the StxScript source. The control flow shape is preserved — every if (cond) return err(x) in the source is a top-level if in the output, with the error path on one branch and the rest of the function on the other. The let for current in the source becomes a Clarity let binding. The two updates inside begin are sequenced exactly as you wrote them.
Step 7: test
stxscript test
The scaffold ships with some example tests. They run against the generated Clarity, so a passing test means the contract behaves correctly in the runtime, not just at type-check.
If you have a Clarinet project, drop build/my-token.clar into contracts/ and Clarinet will run its simulation suite against it. StxScript does not replace Clarinet — they compose.
Step 8: watch mode
When you are iterating, run:
stxscript watch src/
That rebuilds the Clarity output every time you save a .stx file. Pair it with your editor’s LSP integration and the feedback loop is: type, save, see the new Clarity in the build folder, run the tests.
What just happened
You installed a Python package, scaffolded a project, edited a TypeScript-flavored source file, ran a static checker, transpiled to Clarity, and ended up with a .clar file that deploys to Stacks. Nothing in that flow required learning the Lisp surface syntax of Clarity. Nothing required imagining what the runtime would do — you can read it.
If you want to deploy this contract to testnet, that step is out of scope for the StxScript project (use the standard Stacks CLI or Clarinet’s deployment helpers), but the artifact you produce is ready for either.
If you find something the transpiler does badly — surprising output, a feature that should work and does not, an error message that does not help — file an issue with the StxScript source and the Clarity output. That is the highest-signal feedback the project gets, and it is how the next version of the language gets better.
The best way to learn the rest is to scaffold the nft and defi templates next and read their generated Clarity. By the third one, you will stop opening the .clar files because you will already know what they say.