Use this page to understand what a token is on this chain before you create one, list one or index one. RPL is Rand’s token standard, new on chain 14. A token is a shielded native asset: one entry in a ledger registry, with balances held as ordinary notes in the same commitment tree RAND uses. It is not a contract. To create one step by step, see Deploy an RPL token.
The reason is structural. A Call writes only a receipt, so an ERC-20-style contract had nowhere to keep a balance, and a public balance table would make tokens the one transparent asset on a shielded chain. RPL makes a token a first-class ledger object instead. Chain 20 added RPL-2 (v0.6.8): a program can keep public state cells and a public vault, and can be the mint authority of a token of its own (below); balances held by people are still notes. Chain 21 (fullnode v0.8.0, live since 2026-10-12) adds two more public holders of tokens beside the vaults: RPL-3 perps, a perpetual-futures engine whose one market, BTC-PERP, is collateralised in zUSD, and multisig accounts, M-of-N Dilithium2 accounts with per-asset public balances (Transactions).
A token is a registry entry
TokenRegistry (crates/randprotocol-core/src/ledger/tokens.rs) is one table shared by every token, native or bridged. It is the same registry the bridge resolves an asset’s index through. Each entry:
| field | rule |
|---|---|
id | the asset id, 32 bytes, content-addressed |
index | u32, the value a note’s asset word carries; dense, assigned in registration order from 1. Index 0 is RAND and is never registered |
name | 1 to 32 bytes of UTF-8, spaces allowed, immutable |
symbol | 1 to 12 ASCII graphic bytes (0x21..=0x7e, no whitespace), immutable |
decimals | 0 to 9, immutable |
authority | the mint authority, below |
mint_nonce | the replay counter of a Key authority |
total_supply | u64, checked arithmetic; an overflow refuses the mint. Since chain 15 it is held below 2^63 (below) |
registered_at | the height of the registration |
Decimals stop at 9 because amounts are u64: at 18 decimals a u64 holds only 18.4 whole tokens, and 9 is RAND’s own precision. A bridged token always registers at 8 decimals, whatever its source coins use.
The registry is switched on by a tokens section in the genesis file, as bridge and aggregation are by theirs. Without it every token action is refused before any other check (TokenError::Disabled). Chain 21’s genesis has the section and lists one token, zUSD, with its seven backings, carried from chain 14 through each cut since; every other token on the chain arrives by transaction, and none carries over a cut: the eleven tokens registered on chain 20 after its genesis (durian.market’s DUR and its LP shares, the example-program tokens, RUSD) were dropped at the chain-21 cut with their notes, since none was bridge-backed. (Chain 14’s genesis listed none.) With the gate on, a tokens_root (the merkle root of blake3("rand-token-leaf-1", bincode(TokenInfo)) leaves in index order) folds into the state root. The root’s domain is rand-state-4 on a chain with nothing later switched on; chain 20’s root also carried the faucet and bond-queue state, the live gas prices and the program-state root, hashed under rand-state-8, and was wrapped once more by the admitted-validator set; chain 21’s adds the perps state (rand-state-9) and the multisig register (rand-state-multisig-1) (Consensus).
A chain with a tokens section also requires every genesis alloc note to carry its opening (pk, time, r), and the node recomputes the commitment and refuses a file that does not match. The commitment is at asset 0 unless the opening names the index of a token the same genesis lists — chain 21 has three such notes, 10, 89.002999 and 0.999 zUSD at index 1 (chain 20 had two, 10 and 89) — and per listed token the genesis notes must sum to what its backings start with locked. Once a note has an asset word that can name a token, an unopened genesis commitment would be the one way to put an unbacked note of any asset into the pool unchecked.
Asset id, index and rpl1…
A token has two names, and the RPC gives both.
- The asset id is 32 bytes, content-addressed like a program id. A native token’s id is
blake3("rand-rpl-asset", bincode(name, symbol, decimals, authority, initial, salt)), wheresaltis 32 bytes the creator chooses andinitialis the whole initial mint, if any. A bridged token’s id isblake3("rand-rpl-asset", bincode(name, symbol, 8, salt)). Registering an id that already exists is refused (TokenError::AlreadyRegistered). - The index is what a note actually carries, because a 32-byte id does not fit in one word. It is a shorthand that means something on this chain only.
- The text form
rpl1…is bech32m (BIP-350) with human-readable partrplover the 32 id bytes: 52 data characters and a 6-character checksum, 62 characters in all. It is shown beside the 64-hex id wherever a token is named and accepted wherever one is looked up; hex stays accepted. Parsing is strict: a bad checksum, the wrong prefix, a payload that is not exactly 32 bytes, or mixed case is refused, so a typo cannot resolve to a different token.
Symbols are not unique. As with ERC-20 and SPL, a token’s identity is its asset id, never its name or ticker.
The old per-(chain, token) value that rand_bridgeAssetId computes and rand_getAssets prints per backing row is a narrower identifier for the bridge’s wire format, not the token’s own id.
Mint authorities
| authority | who can issue units | notes |
|---|---|---|
None | nobody, after registration | fixed supply: the initial mint is required and is all there will ever be |
Key(PublicKey) | the holder of one Dilithium2 key | mints again later by signature; can hand the authority to another key or renounce it |
Bridge { backings } | the bridge’s lock and release only | never a direct mint; exists only through the bridge’s governance actions |
Program(ProgramId) | that program’s Invokes only (RPL-2) | on a chain with a program_state section (chains 20 and 21): registered with no initial mint, so its supply starts at zero, and minted or burned only by transitions the program’s proof vouches for (rand token create --program <id>). Refused at registration on a chain without the section |
A RegisterToken naming Bridge is refused (TokenError::AuthorityNotAllowed), as is one naming Program with an initial mint or on a chain without program_state. A Key authority is exactly one Dilithium2 public key: a key of any other byte length is refused (TokenError::BadAuthorityKey), and the recipient’s ML-KEM key in a mint is held to its length the same way. Both are consensus state or block space bought at a flat fee, so neither may be arbitrarily large.
Creating a token
Creation is permissionless. Action::RegisterToken { name, symbol, decimals, authority, initial, salt, index } rides an ordinary RAND fee bundle and pays BUNDLE_BASE plus the registry’s registration_fee. The registration fee is a genesis parameter bounded between 1 RAND and 10,000 RAND, so a spam token always costs something. On chain 20 it is 1 RAND, so a registration costs 1.001 RAND, and chain 20 burns it, as every chain since 15 has: the proposer keeps only the bundle base (below).
index names the registry index the creator sealed the initial note for. If another registration took that index first, the transaction is refused (TokenError::IndexMismatch) rather than stranding the note; the creator proves again at the next index.
The minted note is computed by the chain, not chosen by the submitter: note_commitment(recipient.pk, MINT_FROM, amount, index, time, r). The amount and the recipient’s pk are public on the wire, so the recipient can rebuild the note from the transaction’s own fields even if the sealed envelope is garbage. A bridge deposit has the same defence.
Why the id binds the whole initial mint
A RegisterToken is unsigned. The transaction binding stops a proved copy from being altered, but says nothing about what a fresh copy may name. Had the id covered only the initial amount, an observer of a pending registration could submit a copy with a different recipient, register it under the same id, and take a fixed-supply token’s whole supply by committing first. Because the id covers the whole initial mint (amount, recipient, r, time and envelope), a redirected copy hashes to a different asset id: a different token with the same symbol, which a non-unique symbol already permits. The original still lands at the next index for the cost of one re-proof.
Transfers are the same bundle as RAND
Since chain 14 every value-moving transaction carries exactly one bundle, the fixed 4-in-4-out hidden-asset shape (Transactions and actions). Slots 0–1 carry a private asset, RAND or any RPL token, and the circuit never learns which. Slots 2–3 always carry RAND. A transfer of any asset is Action::None, the action a plain RAND payment uses. There is no token-transfer action and no public field naming the token.
What an observer of a token transfer sees is exactly what a RAND payment publishes: an anchor, four nullifiers, four commitments, the RAND fee, burn_a = burn_r = burn_asset = 0, a time, four envelope ciphertexts and the bundle proof. Since chain 17 (split authorisation) every bundle also carries an auth_commit and a second, smaller auth proof, again the same for every asset. On chain 20 each envelope is exactly 1,860 bytes and has room for an encrypted memo (rand send --memo), whether or not one is sent. It does not see the sender, the recipient, the amount, which asset moved, which slots were dummies, or the change.
The wallet spends up to two notes of the token in slots 0–1 and up to two RAND notes in slots 2–3 for the fee. The fee is always RAND, so a holder with a token and no RAND is refused before proving. The cost is the same as a RAND payment’s (Fees). Since chain 17 that is two STARK proofs made on the sender’s machine; measured on chain 20, the spend authorisation (tier 10, about 7 s, about 1.37 MB) and the bundle (tier 14, about 100 s, about 1.5 MB), about 2.85 MB on the wire together, each under that chain’s 4 MiB cap on one proof. Chain 21’s proofs are constraint set 9’s, under a 6 MiB cap, and are not measured here yet. Before chain 17 a transfer carried the bundle proof alone, about 1.42 MB as measured then.
Minting and the authority
TokenMint { asset, amount, recipient, r, time, envelope, nonce, signature } mints more of a Key-authorised token. SetAuthority { asset, new, nonce, signature } hands the authority to another key, or renounces it when new is absent; after a renounce no key can ever mint the token again. Either action on a token whose authority is not Key is refused (TokenError::NotKeyAuthority).
The authority signs blake3("rand-rpl-mint-1" | "rand-rpl-authority-1", chain_id ‖ asset_id ‖ nonce ‖ body). nonce must equal the token’s mint_nonce, which then increments, so a signed mint cannot be replayed on this chain or carried to another. A mint’s body includes the digest of the envelope: the commitment alone does not cover it, and without the digest a third party could re-wrap a gossiped mint with a garbage envelope and spend the nonce.
Both actions ride a RAND fee bundle paying BUNDLE_BASE. The wallet paying the fee and the key authorising the mint are separate and can belong to different people.
Burning
Action::TokenBurn { asset, amount } rides the same single bundle: the bundle’s burn_asset must equal asset, burn_a must equal amount, and burn_r is 0. The token leaves the pool through the asset slots and the RAND fee through slots 2–3 of the same proof. total_supply falls by exactly amount. A Bridge-authority token is refused here (TokenError::BridgedToken); it leaves through a bridge burn.
Fees
Every RPL action pays in RAND.
| action | fee floor | on chain 21 (as on chain 20) |
|---|---|---|
a transfer of any asset (Action::None) | BUNDLE_BASE | 0.001 RAND |
RegisterToken | BUNDLE_BASE + registration_fee | 1.001 RAND, of which 1 RAND is burned |
TokenMint | BUNDLE_BASE | 0.001 RAND |
SetAuthority | BUNDLE_BASE | 0.001 RAND |
TokenBurn | BUNDLE_BASE | 0.001 RAND |
Chain 21’s genesis has a gas section, as chain 20’s did, and it prices program calls and invokes only. These floors are flat: no RPL action pays by gas or by byte. On chain 21 every floor is burned and the proposer keeps the tip (Fees); a registration’s 1 RAND was burned already.
What is public and what is hidden
| public | hidden | |
|---|---|---|
| registration | name, symbol, decimals, authority, the asset id, the initial mint’s amount and recipient | nothing; this is how a token’s supply is auditable from its first block |
| a further mint | the amount, the recipient’s shielded address, the new note’s fields | who paid the RAND fee |
| a transfer | nothing that says a token moved at all | sender, recipient, amount, which asset |
| a burn | the asset and the amount | who held the burned notes |
| an authority change | the new key, or that it was renounced | — |
total_supply is therefore always auditable: it is what registration and every mint added, less every burn. A transfer between holders reveals nothing, including that a token rather than RAND changed hands.
Disclosure works as it does for RAND. The note plaintext carries the asset, so a per-transaction key (rand tx-key <hash>) or a viewing key opens a token note exactly as it opens a RAND one, and rand_checkTransaction and rand_getViewingNotes return asset in the note they disclose. Given such a key, randscan resolves the index against the registry and shows the transfer as the named token; the public page for the same transaction shows none of it.
Bridged tokens
A Bridge-authority token is still an RPL token: the same registry entry, the same hidden transfer, the same burn mechanics. Its supply moves only through a lock on a source chain (BridgeAttest) or a release (BridgeBurn). One bridged token can have many backings, each a (chain, token) pair with its own source decimals, locked amount and mint cap, and total_supply always equals the sum of what its backings have locked. A bridged token is registered, and a backing added, only under a quorum of the bridge’s post-quantum guardians.
On chain 21 the registry’s first token is zUSD (“Shielded USD”), index 1, 8 decimals, listed in the genesis file with seven backings (USDT and USDC on Ethereum, Binance Smart Chain and Solana, and USDT on Tron). It keeps the asset id it had on chain 14, where it was registered by transaction at height 256: chains 15 to 21 each list it in the genesis under the same name, symbol and salt. It started chain 21 with 100.001999 zUSD in three genesis notes (10, 89.002999 and 0.999), backed by 22 USDT locked on Ethereum, 23.001999 on Binance Smart Chain, 30 on Tron and 25 on Solana, carried from chain 20’s bridge state at height 814,702. Chain 20 had started with 99 zUSD in two notes (10 and 89), carried from chain 19. Chain 19 started with 10 zUSD, backed by 10 USDT on Solana; chain 18 with 9 USDT locked on Tron and 1 on Solana. Its total supply equals what its backings hold locked and moves with every deposit and redemption. Its id is rpl1xtj6k2x8strx8c2d5fjs50ltztck5ykms4ve7nmzmstf6fhn0v0spelqtx. Bridge covers backings, release units, mint caps, the pause and the co-signature.
zUSD is a basket, not seven separate claims. Its backings are pooled at face value, so a holder bears the risk of every listed backing, not of the coin they deposited: a backing that has lost its peg can be deposited at par and the zUSD burned toward a sound backing, until the sound ones are drained. What bounds the loss is the rolling mint window (on chain 21, as on chain 20, 4,000 zUSD across all backings per 24 hours, beside 100,000 per backing) and the custody actually locked. The window limits the rate and gives time to pause mints. It does not cap the total. A backing is listed only under the post-quantum guardian quorum, and not while mints are paused.
The bridge’s fees, in zUSD (since chain 20). Chain 20’s genesis set bridge.fees, and chain 21’s carries it unchanged: 10 bps on a deposit and 10 bps on a burn, each rounded down to a whole release unit of the backing (100 base units for a 6-decimal USDT), paid as a zUSD note to a treasury address the genesis names (v0.6.8, fullnode docs/bridge.md §25). A deposit of g appends two notes: the depositor’s g − fee and the treasury’s fee; 1 USDT in is 0.999 zUSD to the depositor. A burn of amount keeps fee as a treasury note and releases amount − fee on the source chain; the guardians sign that release amount, and rand_getBridgeBurn serves amount, release_amount and fee. A fee note is issued and backed, so total_supply still equals the sum of the backings’ locked: a deposit moves both by g, a burn lowers both by amount − fee. A fee note carries no envelope. Every word of it is public (the amount from the transaction and the genesis rate, the asset and time, a blinding derived from the attestation digest or the burn’s transaction id, the owner from the genesis), so the treasury’s wallet rebuilds it from public fields (Notes). BRIDGE_BURN_FEE, the 0.01 RAND a burn pays the proposer, is unchanged. Chains 14 to 19 had no bridge fee on Rand.
The registry since chain 15
Since chain 15 the genesis switches on three registry rules chain 14 did not have, and chain 21’s genesis, like chain 20’s, sets the same three values. Each is committed to the genesis hash and the token root.
| genesis field | chain 21 (as chain 20) | effect |
|---|---|---|
tokens.max_tokens | 4096 | the registry never holds more; at the cap RegisterToken and RegisterBridgedToken are refused RegistryFull. ListBacking adds no token and is unaffected |
tokens.burn_registration_fee | true | a registration’s registration_fee is destroyed instead of paid to the proposer, who keeps fee − registration_fee. rand_getSupply counts it in burned and in registration_fees_burned (Supply) |
tokens.bound_note_value | true | a TokenMint, an initial mint or a bridge deposit of 2^63 or more is refused AmountTooLarge, and one that would take a token’s total_supply to 2^63 or past it SupplyTooLarge |
The note-value bound exists because the bundle circuit range-checks every amount to 63 bits: a note worth 2^63 or more could be committed but never spent, while still counting in the token’s supply. Every node also refuses such an amount at admission on every chain, whatever its genesis says.
RPC and CLI
| method | params | result |
|---|---|---|
rand_getTokens | [from_index, limit], optional, default 0 and 1000, clamped to 1000 | { enabled, registration_fee, next_index, max_tokens, burn_registration_fee, bound_note_value, tokens }, ascending by index, read by range from from_index |
rand_getToken | [token]: an index, a 64-hex id or rpl1… | one token’s row, or null; a malformed id is -32602 |
rand_getTokenSupply | [token], same forms | { total_supply, backings }; backings is empty for a native token |
Every amount in these results (total_supply, registration_fee, a backing’s locked, mint_cap_per_day and minted_today) is a decimal string, because a JSON number is not exact past 2⁵³. index, next_index, mint_day and max_tokens are plain integers (max_tokens is null on a chain without a cap); burn_registration_fee and bound_note_value are booleans. A row’s authority is {"kind":"none"}, {"kind":"key","key","address"}, {"kind":"bridge","backings":[…]} or {"kind":"program","program"}. Full detail is in JSON-RPC.
Privacy. rand_getTokens costs the same whichever token the caller has in mind. rand_getToken and rand_getTokenSupply tell the node which token the caller cares about: fine for an explorer, wrong for a wallet about to send. The wallet therefore resolves every --asset from the whole listing, and a numeric index never reaches the node at all.
| command | what it does |
|---|---|
rand token create | register a token at the registry’s next index, fixed-supply or Key-authorised |
rand token mint | mint more of a Key-authorised token, signed by its authority key |
rand token set-authority | hand the authority to another key (--new-key) or give it up (--renounce) |
rand token burn <ASSET> <AMOUNT> | destroy units this wallet holds |
rand token info <ASSET> | one token’s full row |
rand token list | one page of the registry |
rand send <TO> <AMOUNT> --asset <INDEX|rpl1…|hex> | send a token in one bundle; without --asset it sends RAND. It prints the recipient’s fingerprint and the amount and asks send? [y/N] before proving (--yes skips the question) |
rand asset-balance [INDEX] | what this wallet holds of one asset, or a row per asset |
rand token register-bridged, rand token list-backing | the bridge’s two listing actions; each needs a PQ guardian quorum file |
Since v0.5.10 the amounts of rand send --asset, rand token mint and rand token burn are in the token’s display units, read at its registry row’s own decimals: 1.5 of a 6-decimal token is 1,500,000 of its smallest unit, and more fractional digits than the token has is refused. send’s confirmation prints both (1.500000 USDX (1500000 units)). rand token create’s --fixed-supply and --initial still take whole smallest units, as does rand bridge-burn, and rand asset-balance prints smallest units. On chain the amount is always the smallest-unit integer. Flags and defaults are in Command line.
Differences from ERC-20 and SPL
| ERC-20 / SPL | RPL | |
|---|---|---|
| balances | a public mapping or account | shielded notes; only a key holder can see one |
| transfer events | public, with sender, recipient and amount | none; a transfer publishes nothing distinguishing |
| per-token code | possible: tax, rebase, hooks, freeze | none; every token follows the same fixed rules |
| holding by a program | yes | since chain 20 (RPL-2): a program’s public vault, filled by an invoke’s burn fields and paid out as chain-computed notes |
| holding by a multisig account or the perps engine | yes | on chain 21 (v0.8.0): a multisig account’s per-asset balance, public like a vault’s, and the perps engine’s zUSD collateral; a perp payout is a note the chain computes, like a vault’s |
| amounts | up to 256-bit (ERC-20), 64-bit (SPL) | u64, at most 9 decimals |
| receiving | ERC-20 needs no setup; SPL needs an associated token account | no setup; a shielded address receives anything |
| cost of a transfer | plain EVM or SVM execution | proved by the sender: the shared hidden-asset bundle, and since chain 17 a small authorisation proof beside it |
| approvals | approve / transferFrom | not yet |
| metadata | mutable in some implementations | on chain and immutable |
| freeze, blacklist | possible on some tokens | out of scope by design |
Known limits
- No allowances.
approveandtransferFromare designed in the RPL spec as earmarked allowance accounts and are not built. - A program’s holdings are public. Since chain 20 a program can hold tokens in its vault and mint its own token (RPL-2), but a vault balance is public, like a token’s supply; only notes are private. Chains before 20 had no program state at all, and chain 21’s multisig accounts and perps engine hold tokens the same public way.
- Fees are RAND only. A token cannot pay its own fee; a token-only wallet cannot move.
u64amounts and at most 9 decimals. An 18-decimal source coin is represented at lower precision; the bridge settles this with 8 decimals and a release unit per backing.- Every transfer is proved. About 100 s and several GB of memory on the sender’s machine; there is no cheaper path for a token than for RAND.
- No freeze, blacklist or pause on a native token, by decision. The bridge’s mint pause applies to bridged mints only.
- Per-token lookups leak interest.
rand_getToken,rand_getTokenSupplyand the explorer’s/tokens/:idtell the server which token was asked about.