1. CIP-20 tokens — examples/01-tokens
The key idea: CIP-20 tokens are not actor contracts. Unlike ERC-20 (a
Solidity contract per token), a Cowboy token is a first-class chain primitive —
create / transfer / approve / mint / burn are native validator instructions,
and balances live in a global Token Registry, not in any actor’s storage.
That design choice has consequences worth internalizing:
- No contract to deploy.
cowboy token create --name "My Token" --symbol MTK --decimals 18 --initial-supply 1000000mints a token in a single system tx; there is no bytecode and no per-token storage to rent. - The validator enforces invariants (overflow, underflow, frozen accounts) —
you cannot write a buggy
transferthat loses funds, because you don’t writetransferat all. - Actors compose with tokens via host functions, not message calls:
These run inside the PVM and settle atomically with the rest of the handler — the basis for DeFi-style composability (see
examples/02-liquidity-pools).
transfer_hook: it names an actor the validator calls on
every transfer, letting you implement allowlists / fee-on-transfer / freezes
without owning the token logic. It is the one place token behavior becomes
programmable — use it sparingly, because it runs on every transfer and is
metered against the transfer’s gas.
2. Self-rescheduling timers — examples/21-pure-timer-scheduler
This actor advances a counter purely by rescheduling itself — no external
keeper, no cron. It is the canonical pattern for “do X every N blocks.”
The mechanics that matter:
- Block height, never wall-clock. Timers fire at a block height
(
get_block_height() + delay), because wall-clock time is nondeterministic. Useget_timestamp_ms()only for display, never for scheduling logic. _handlerrouting. A timer payload of the form{"_handler": "<name>", ...}makes the timer invoke that named handler on fire (rather than the defaulthandle_timer) — so one actor can schedule several distinct callbacks.fee_payeris a deliberate choice.schedule_timer_ex(..., fee_payer=sender)pre-charges each fire to the original caller, so the actor doesn’t silently drain its own balance keeping a loop alive. A timer whose fee-payer can no longer fund a fire self-destructs — the loop stops cleanly instead of erroring forever. (System actors paying themselves are a special case — the reserved0x01..=0x0Fband is rejected as afee_payer.)- One-shot, so re-arm explicitly. Each timer fires once; to keep a cadence, the fire handler schedules the next one. There is no “recurring timer” flag — the re-arm is in your handler, which keeps the control flow visible.
3. Runner continuations — examples/20-minimal-runner-continuation
Off-chain work (an LLM call, an HTTP fetch) can’t block a handler — the block
must finalize. Cowboy’s answer is the continuation: you write straight-line
async/await code, and the SDK compiles it into a resumable state machine.
- The
awaitis a suspension point, not a thread block. Atawait runner.http(...)the handler returns; the job is dispatched to an off-chain runner; a later block delivers the result and the SDK resumes the code after theawait. You do not hand-write the resume method —@runner.continuationgenerates the state machine at import time. capture()is mandatory for anything crossing the await. Local variables don’t survive suspension automatically — only fields you stash on thecapture()context (ctx.request_id) are serialized into the continuation state and available on resume. Forgetting this is the #1 continuation bug.- The compiler enforces determinism limits (see the
PVM reference): no more than 8
sequential awaits per function, no
awaitinside a nested function or a bare loop (use@bounded_loop), no generators. These exist because the state machine must be finite and serializable. - Finalize is ordinary, synchronous code.
_finalize_refreshwrites state and emits an event — it runs in the resume frame like any handler, so the normal gas/determinism rules apply.
runner.agent(...) (LLM tool-calling over
mounted CBFS volumes) and the llm_chat example — await an off-chain result,
resume deterministically when it lands.
