Rand Protocol
Documentation
Core concepts

RPL tokens

The RPL token standard - a token as a ledger registry entry, its asset id, index and rpl1 text form, the mint authorities, what creation costs, why a token transfer looks exactly like a RAND payment, mints and burns, and how it differs from ERC-20 and SPL.

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:

fieldrule
idthe asset id, 32 bytes, content-addressed
indexu32, the value a note’s asset word carries; dense, assigned in registration order from 1. Index 0 is RAND and is never registered
name1 to 32 bytes of UTF-8, spaces allowed, immutable
symbol1 to 12 ASCII graphic bytes (0x21..=0x7e, no whitespace), immutable
decimals0 to 9, immutable
authoritythe mint authority, below
mint_noncethe replay counter of a Key authority
total_supplyu64, checked arithmetic; an overflow refuses the mint. Since chain 15 it is held below 2^63 (below)
registered_atthe 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)), where salt is 32 bytes the creator chooses and initial is the whole initial mint, if any. A bridged token’s id is blake3("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 part rpl over 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

authoritywho can issue unitsnotes
Nonenobody, after registrationfixed supply: the initial mint is required and is all there will ever be
Key(PublicKey)the holder of one Dilithium2 keymints again later by signature; can hand the authority to another key or renounce it
Bridge { backings }the bridge’s lock and release onlynever 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.

actionfee flooron chain 21 (as on chain 20)
a transfer of any asset (Action::None)BUNDLE_BASE0.001 RAND
RegisterTokenBUNDLE_BASE + registration_fee1.001 RAND, of which 1 RAND is burned
TokenMintBUNDLE_BASE0.001 RAND
SetAuthorityBUNDLE_BASE0.001 RAND
TokenBurnBUNDLE_BASE0.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

publichidden
registrationname, symbol, decimals, authority, the asset id, the initial mint’s amount and recipientnothing; this is how a token’s supply is auditable from its first block
a further mintthe amount, the recipient’s shielded address, the new note’s fieldswho paid the RAND fee
a transfernothing that says a token moved at allsender, recipient, amount, which asset
a burnthe asset and the amountwho held the burned notes
an authority changethe 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 fieldchain 21 (as chain 20)effect
tokens.max_tokens4096the registry never holds more; at the cap RegisterToken and RegisterBridgedToken are refused RegistryFull. ListBacking adds no token and is unaffected
tokens.burn_registration_feetruea 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_valuetruea 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

methodparamsresult
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.

commandwhat it does
rand token createregister a token at the registry’s next index, fixed-supply or Key-authorised
rand token mintmint more of a Key-authorised token, signed by its authority key
rand token set-authorityhand 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 listone 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-backingthe 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 / SPLRPL
balancesa public mapping or accountshielded notes; only a key holder can see one
transfer eventspublic, with sender, recipient and amountnone; a transfer publishes nothing distinguishing
per-token codepossible: tax, rebase, hooks, freezenone; every token follows the same fixed rules
holding by a programyessince 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 engineyeson 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
amountsup to 256-bit (ERC-20), 64-bit (SPL)u64, at most 9 decimals
receivingERC-20 needs no setup; SPL needs an associated token accountno setup; a shielded address receives anything
cost of a transferplain EVM or SVM executionproved by the sender: the shared hidden-asset bundle, and since chain 17 a small authorisation proof beside it
approvalsapprove / transferFromnot yet
metadatamutable in some implementationson chain and immutable
freeze, blacklistpossible on some tokensout of scope by design

Known limits

  • No allowances. approve and transferFrom are 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.
  • u64 amounts 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_getTokenSupply and the explorer’s /tokens/:id tell the server which token was asked about.

Last verified against fullnode commit c0878c3f (chain 21) on 2026-10-12, and fullnode `docs/tokens.md`, `docs/cli.md`, `docs/rpc.md`, `docs/superpowers/specs/2026-09-19-rpl-token-standard-design.md`, and the live registry at `randscan.org/api/v1/tokens` on 2026-09-20; updated for chain 15 against fullnode `8abf85e` (v0.5.9) `docs/tokens.md` §§14–16 and `deploy/genesis-chain15.json`; updated for chain 18 on 2026-09-30 against fullnode `90e7716` (the fleet runs v0.6.7 `86941a1`) `docs/tokens.md`, `docs/cli.md`, `docs/shielded.md` §2, `docs/bridge.md` §13.1, `crates/randprotocol-core/src/gas.rs` and `deploy/genesis-chain18.json`; updated for chain 20 on 2026-10-01 against fullnode v0.6.8 `c9c9bd3c` `docs/bridge.md` §25, `docs/program-state.md`, `crates/randprotocol-core/src/ledger/tokens.rs`, `crates/randprotocol-client/src/wallet.rs` (`rebuilt_notes_with`), the committed `deploy/genesis-chain20.json` and `rand_getLimits` on chain 20. Where the code and this page disagree, the code is right; tell us.