Docs · 03 / 8

Program format

The JSON language reference: storage, meta, read, write, init and expressions.

Top level

A program is one JSON object with these keys: v, type, storage, meta, read, write and an optional init.

storage

Maps a slot name to a type.

TypeMeaning
uintUnsigned integer
stringString
booleanBoolean
addressAddress
map:<type>Map from key to value of <type>
map:map:<type>Nested map. Maximum depth is 2

A missing value reads as the type's zero.

meta

Constants: name, symbol, price, fee receiver address, limits. The type comes from how the value is written:

Written asType
A number or digit stringuint
A bc1…, tb1… or bcrt1… stringaddress
true / falseboolean
Anything elsestring

read methods

shape
{ "args": ["name:type"], "return": "<expr>" }

args is optional. Read methods run without a transaction, so $tx is not available in them.

write methods

shape
{
  "args": ["name:type"],
  "require": [["<cond>", "<message>"]],
  "set": [ /* steps */ ]
}

A step is one of:

steps
["$storage.slot", "<expr>"]
["$storage.map[key]", "<expr>"]
{ "if": "<cond>", "then": [ /* steps */ ], "else": [ /* steps */ ] }

else is optional; chain with another if inside else. Conditions and assignments are type-checked in every branch when the program is compiled.

init

The same steps as set, run once at deploy. There is no transaction and no arguments; it can read $program, $meta and $storage.

Argument declarations

Declared as "name:type", where type is uint, address, string or boolean.

Expressions

KindOperators
Arithmetic (integers)+ - * / %. / is floor division; division by zero rejects.
Comparison< <= > >= == !=
Logic&& || !
Unary-
Ternarya ? b : c

Variables

VariableContent
$storageStorage slots
$argsDeclared method arguments
$metaConstants
$programdeployer, id
$txThe action transaction (write methods only)

$tx

Limits

LimitValue
Methods32 at most
Storage slots32
Assignments per method16
Steps per method32
Branch depth3
require per method8
Expression nodes in total256
Messagesup to 128 chars
Stringsup to 128 chars
Valuesup to 2^256 - 1
Names[A-Za-z_][A-Za-z0-9_]{0,31}

Every value is bounded, so state can never blow up.

Determinism

The VM is pure. Executing a write method never mutates the input storage; it returns the list of writes to apply.

Example: a fungible token

A token with these methods:

program.json
{
  "v": 1,
  "type": "token",
  "storage": {
    "balances": "map:uint",
    "allowances": "map:map:uint",
    "minted": "uint",
    "burned": "uint"
  },
  "meta": {
    "name": "Example",
    "symbol": "EXM",
    "decimals": 8,
    "totalSupply": "2100000000000000",
    "lim": "100000000",
    "price_sats": 1000,
    "fee_receiver": "bc1p<fee-receiver-taproot-address>"
  },
  "read": {
    "balanceOf": {
      "args": ["owner:address"],
      "return": "$storage.balances[$args.owner]"
    },
    "totalSupply": { "return": "$storage.minted - $storage.burned" },
    "name": { "return": "$meta.name" },
    "symbol": { "return": "$meta.symbol" }
  },
  "write": {
    "mint": {
      "args": ["amt:uint"],
      "require": [
        ["$args.amt > 0", "amount must be positive"],
        ["$args.amt <= $meta.lim", "above per-call limit"],
        ["$storage.minted + $args.amt <= $meta.totalSupply", "supply cap reached"],
        ["$tx.outputs[2].address == $meta.fee_receiver", "pay the fee receiver at output 2"],
        ["$tx.outputs[2].value >= $meta.price_sats", "payment below price"]
      ],
      "set": [
        ["$storage.balances[$tx.inputs[0].address]",
         "$storage.balances[$tx.inputs[0].address] + $args.amt"],
        ["$storage.minted", "$storage.minted + $args.amt"]
      ]
    },
    "transfer": {
      "args": ["amt:uint"],
      "require": [
        ["$storage.balances[$tx.inputs[0].address] >= $args.amt", "insufficient balance"]
      ],
      "set": [
        ["$storage.balances[$tx.inputs[0].address]",
         "$storage.balances[$tx.inputs[0].address] - $args.amt"],
        ["$storage.balances[$tx.outputs[2].address]",
         "$storage.balances[$tx.outputs[2].address] + $args.amt"]
      ]
    },
    "burn": {
      "args": ["amt:uint"],
      "require": [
        ["$storage.balances[$tx.inputs[0].address] >= $args.amt", "insufficient balance"]
      ],
      "set": [
        ["$storage.balances[$tx.inputs[0].address]",
         "$storage.balances[$tx.inputs[0].address] - $args.amt"],
        ["$storage.burned", "$storage.burned + $args.amt"]
      ]
    },
    "approve": {
      "args": ["spender:address", "amt:uint"],
      "require": [],
      "set": [
        ["$storage.allowances[$tx.inputs[0].address][$args.spender]", "$args.amt"]
      ]
    }
  }
}

Annotations

  1. storage declares balances as a map keyed by identity address, allowances as a nested map (depth 2), and two counters.
  2. meta.fee_receiver is typed address because it starts with bc1; price_sats is a uint. Replace the placeholder with a real address.
  3. In mint, the require list checks the limit, the supply cap, and that output 2 pays at least the price to the fee receiver. Only then do the set steps credit the sender.
  4. In transfer, the debit runs before the credit, and each step sees the previous write, so sending to yourself is safe.
  5. The receiver of a transfer is whatever address output 2 decodes to. See Receiver address types.
  6. approve only records an allowance; this example defines no method that consumes it.