Normative specification · Version 2026.09.01

SRC-20 specification

SRC-20 is a fungible token protocol on Bitcoin. Its operations are plain JSON documents embedded in transaction output scripts, which every full node must keep, rather than in witness data that a node is free to discard.

How to read this document

Every rule below is numbered and traceable to the implementation that enforces it. The reference implementation is the Bitcoin Stamps indexer, stampchain-io/btc_stamps; the constants quoted here were read from indexer/src/config.py, indexer/src/index_core/src20.py, indexer/src/index_core/transaction_utils.py, indexer/src/index_core/script.py and indexer/src/index_core/models.py at indexer version 1.9.3.

SRC-20 has no formal standards body. In practice the protocol is what the indexers agree on, so where this document states a rule it names the code that produces it. Where implementations could reasonably differ, the document says so instead of inventing an answer.


1. Scope and terminology

This document specifies how an indexer derives SRC-20 token state from the Bitcoin blockchain. It does not specify wallet behaviour, fee policy, or any marketplace protocol.

  • S-1

    SRC-20 is defined on Bitcoin mainnet only. No testnet or signet deployment is recognised by the reference indexer.

  • S-2

    SRC-20 is an account ledger. A balance belongs to a Bitcoin address, not to a specific unspent output. Spending the output that carried an operation does not move, destroy, or otherwise affect the balance.

  • S-3

    The creator (sender) of an operation is the address that controls the output being spent by input 0 of the transaction. Other inputs are irrelevant to protocol semantics.

  • S-4

    The destination (recipient) of an operation is the address encoded in output 0 of the transaction. Later outputs carry data, not intent.

  • S-5

    SRC-20 is a sub-protocol of Bitcoin Stamps. It shares the Stamps data carrier and the stamp: prefix with SRC-721 and SRC-101, and is distinguished from them only by the p field of its JSON payload.

Rules S-3 and S-4 come from get_tx_info() in transaction_utils.py, which derives source from the previous output spent by ctx.vin[0] and destination from ctx.vout[0].scriptPubKey.


2. The data carrier

An SRC-20 operation is a UTF-8 JSON document. To put that document on Bitcoin, it is split across the scripts of transaction outputs. Two carriers exist. Both are still indexed; which one a transaction may use depends on its block height.

Plate I · Carrier A: bare multisig

Bare multisig SRC-20 carrier A Bitcoin transaction. Input zero determines the sender address and also supplies the ARC4 key. Output zero pays the recipient. Outputs one and above are one-of-three bare multisig scripts whose first two public keys carry 31 data bytes each and whose third public key is a recognised burn key. The concatenated data bytes are ARC4 decrypted, then read as a two byte big-endian length, the ASCII prefix "stamp:", and the JSON payload. Transaction input 0 prevout → sender address input 1..n (funding) no protocol meaning output 0 recipient address output 1..n · 1-of-3 multisig pubkey 1 → 31 data bytes pubkey 2 → 31 data bytes pubkey 3 = burn key ARC4 key = input 0 txid, byte-reversed concatenated data bytes (ciphertext) ARC4 decrypt len (2 B) "stamp:" {"p":"SRC-20", ...} big-endian uint16 6 ASCII bytes UTF-8 JSON, len minus 6 bytes
Carrier A. The sender address and the decryption key both come from input 0, which is what binds the payload to the spender. The third public key in each multisig output is a provably unusable burn key, which is how the indexer tells a data output from an ordinary payment.

Plate II · Carrier B: OLGA P2WSH

OLGA pay-to-witness-script-hash SRC-20 carrier A Bitcoin transaction where output zero pays the recipient and every later output is a pay-to-witness-script-hash output whose 32 byte witness program is raw payload data. The programs are concatenated, trailing zero padding is removed, and the result is read as a two byte big-endian length, the ASCII prefix "stamp:", and the JSON payload. No encryption is used. Transaction outputs output 0 recipient address output 1..n · P2WSH witness program = 32 B data witness program = 32 B data ... zero padded to 32 B concat, strip 0x00 len (2 B) "stamp:" {"p":"SRC-20", ...} no ARC4: the bytes are plaintext Precedence: if any qualifying P2WSH data output is present, the multisig branch is not attempted, even when the P2WSH length check fails. This exclusion is deliberate and consensus-load-bearing.
Carrier B, known as OLGA. The payload sits in the 32-byte witness program of each P2WSH output. Nothing is encrypted, and the outputs are never spent, so the data stays in the UTXO set.

3. Why the output script matters

This is the design claim that distinguishes SRC-20 from inscription-based token protocols such as BRC-20, and it is worth stating precisely rather than as a slogan.

Plate III · Where the bytes live

Comparison of payload location Two columns. On the left, an SRC-20 payload sits in the scriptPubKey of transaction outputs, which is part of the transaction identifier preimage and enters the unspent output set. On the right, an inscription payload sits in the witness of a transaction input, which is committed by the witness merkle root and may be discarded by a pruned or non-witness-serving node. SRC-20 · output script Lives in scriptPubKey of an output. Committed by the legacy txid preimage. Enters the unspent transaction output set. Never spent, so it is never removed. Present in a block downloaded without witness data. Cost: permanent UTXO set growth. Inscription · input witness Lives in the witness of a spending input. Committed by the witness merkle root. Not part of the UTXO set. A pruned node discards the block body; a node may serve blocks without witness. Cost: cheaper, at the witness discount.
Plate III. The practical difference is who is obliged to keep the bytes. Both payloads are equally committed by the block header, so neither can be forged after the fact.
  • U-1

    SRC-20 payload bytes are part of an output's scriptPubKey. Any node that maintains a full unspent output set holds those bytes for as long as the outputs remain unspent, which in practice is forever, because the outputs are unspendable by construction.

  • U-2

    Neither carrier is spendable in practice. Under Carrier A the third public key of the multisig is a burn key nobody holds a private key for. Under Carrier B the 32 bytes are payload rather than the hash of any real script, so no witness can satisfy them.

  • U-3

    The honest statement of the tradeoff: SRC-20 buys durability by permanently enlarging every full node's UTXO set, and pays full non-witness transaction weight for the privilege. Inscriptions make the opposite trade. Neither payload can be altered after confirmation; the difference is the cost of guaranteed availability, not the strength of the commitment.

  • U-4

    A claim that inscription data "can be deleted" is too strong. Witness data is committed by the block header and is retained by every archival node. The accurate claim is that a validating node is permitted to discard it, while output scripts must be retained to validate future spends.


4. Encoding rules

4.1 Carrier A: bare multisig

  • E-1

    A data output is an output whose script assembles to exactly OP_1 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG. Any other multisig shape is rejected by the decoder.

  • E-2

    pk3 must be one of five recognised burn keys. If it is, the transaction's keyburn flag is set to 1. An SRC-20 operation carried by multisig is only recognised when keyburn is 1. The recognised keys are:

    022222222222222222222222222222222222222222222222222222222222222222
    033333333333333333333333333333333333333333333333333333333333333333
    020202020202020202020202020202020202020202020202020202020202020202
    030303030303030303030303030303030303030303030303030303030303030302
    030303030303030303030303030303030303030303030303030303030303030303
  • E-3

    Data bytes are taken from pk1 and pk2 of every data output, in output order, as pubkey[1:-1]. That is, the leading sign byte and the trailing byte of each 33-byte compressed key are dropped, yielding 31 data bytes per public key and 62 per output.

  • E-4

    The concatenated bytes are decrypted with ARC4. The key is the txid of the output spent by input 0, in byte-reversed form, that is, the txid as normally displayed. This binds the payload to the specific input being spent.

  • E-5

    After decryption, bytes 2 through 7 must equal the ASCII prefix stamp:. If they do not, the transaction is not a Stamps transaction and is ignored.

  • E-6

    Bytes 0 and 1 are a big-endian unsigned 16-bit length covering the prefix and the payload. If the decrypted buffer is shorter than 2 + length, decoding fails with an invalid-data-length error and the transaction is ignored.

  • E-7

    The payload is chunk[8 : 2 + length], that is, everything after the prefix up to the declared length. Trailing bytes beyond the declared length are ignored.

4.2 Carrier B: OLGA P2WSH

  • E-8

    A data output is a P2WSH output, that is, a script of OP_0 <32 bytes>, at output index greater than 0, in a transaction at height 865000 or above. Output 0 is the recipient and is never treated as data.

  • E-9

    The 32-byte witness programs are concatenated in output order and trailing zero bytes are stripped from the end of the concatenated buffer.

  • E-10

    Bytes 0 and 1 are a big-endian unsigned 16-bit length, as in E-6. The slice buffer[2 : 2 + length] must begin with stamp:; the payload is that slice with the prefix removed. No decryption is applied.

  • E-11

    Transactions decoded through Carrier B have keyburn set to 1 by the decoder, since the burn-key requirement of E-2 has no equivalent here.

  • E-12

    Carrier B takes precedence. If a transaction contains any qualifying P2WSH data output, the multisig branch of E-1 is not attempted, even when the P2WSH length check in E-10 fails and the transaction ends up carrying nothing. Implementers must reproduce this exclusion: adding a multisig fallback here changes which transactions are valid and forks the index.

Not a carrier

OP_RETURN outputs do not carry SRC-20 payloads. The indexer notices whether a transaction has an OP_RETURN output, but only to exclude certain classic Stamps; it never reads SRC-20 JSON from one. OLGA transactions commonly contain an OP_RETURN output for unrelated reasons, and the flag is explicitly reset for them.


5. Activation heights

SRC-20 has changed behaviour at specific Bitcoin block heights. An indexer that does not reproduce these heights will not reproduce the ledger.

Consensus-relevant heights, from indexer/src/config.py
HeightConstantEffect on SRC-20
788041CP_SRC20_GENESIS_BLOCKFirst SRC-20 activity. Operations at this stage ride inside Counterparty issuances.
793068BTC_SRC20_GENESIS_BLOCKFirst SRC-20 carried directly on Bitcoin with no Counterparty encoding.
796000CP_SRC20_END_BLOCKEvery Counterparty-encoded SRC-20 operation at or after this height is ignored. From here the protocol is direct-to-Bitcoin only.
833000CP_P2WSH_FEAT_BLOCK_STARTNumeric parsing becomes strict. Below this height a numeric string had every character that was not a digit or a dot silently removed before parsing; at and above it, a malformed numeric string excludes the transaction. See rule N-6.
865000BTC_SRC20_OLGA_BLOCKCarrier B (OLGA P2WSH) becomes available for SRC-20.
The height that catches people out

Height 833000 is not about P2WSH for SRC-20, despite the constant's name. For SRC-20 it is the height at which numeric strings stopped being scrubbed. A payload with "max": "21,000,000" was read as 21000000 before 833000 and is rejected outright after it. Any indexer that applies today's strict rule to historical blocks will produce a different supply for affected tokens.


6. The JSON payload

  • P-1

    The payload is a single UTF-8 JSON object. It is parsed once; a parse failure excludes the transaction silently.

  • P-2

    Numbers in scientific notation are rejected. During JSON parsing, any numeric literal whose text contains e or E raises an error and the whole transaction is excluded. 1e6 is not a valid supply.

  • P-3

    The p field selects the sub-protocol and is compared case-insensitively: a payload is SRC-20 when p lowercased equals src-20. SRC-20, src-20 and Src-20 are all accepted; SRC20 without the hyphen is not.

  • P-4

    The op field is uppercased before dispatch, so deploy, Deploy and DEPLOY are equivalent. Recognised values are DEPLOY, MINT and TRANSFER. Anything else is recorded as an unsupported operation and is invalid.

  • P-5

    Field presence is checked as a superset. A payload qualifies for an operation's shape when it contains at least that operation's required keys. Unknown extra fields do not invalidate a payload; they are carried along and, for DEPLOY, some of them are stored as metadata.

  • P-6

    An empty string value in any field is normalised to null before validation. A field present with value "" is treated as a field that was not supplied.

  • P-7

    Numeric fields accept either a JSON string or a JSON number. Both are converted to arbitrary-precision decimals. Floats are formatted without exponent before conversion.

Difference from BRC-20

BRC-20 uses "p": "brc-20" and requires string values for amounts. SRC-20 uses "p": "src-20" and accepts numbers as well as strings. The field names tick, max, lim, amt look identical between the two protocols, which is exactly why payloads get copied across and then fail: the ticker rules, the decimal defaults and the carrier are all different.


7. Ticker rules

  • T-1

    A ticker is at most 5 code points. The limit is counted in Unicode code points, not bytes, so a five-emoji ticker is legal and occupies 20 bytes.

  • T-2

    A ticker must be non-empty and every character must belong to the allowed set. The allowed ASCII characters are exactly:

    . ! # $ % & ( ) *
    0 1 2 3 4 5 6 7 8 9
    < = > ? @
    A-Z  ^  _  a-z  ~

    Note what is absent: no space, no hyphen, no comma, no plus, no slash, no colon, no quote or bracket characters of any kind.

  • T-3

    In addition, a fixed allowlist of emoji code points is permitted. It is an explicit list of single code points in the U+1F004 to U+1FAD6 range, not a general Unicode category test. Skin-tone modifiers, variation selectors and zero-width-joiner sequences are not in the list, so a composed emoji such as a flag or a family sequence fails T-2.

  • T-4

    Tickers are case-insensitive. The ticker is lowercased before any comparison, so KEVIN, kevin and KeViN are one and the same token.

  • T-5

    Before matching, the ticker string is re-decoded from Latin-1 to UTF-8 where that succeeds. This repairs tickers that were mis-decoded upstream and is part of how emoji tickers survive the pipeline.

  • T-6

    Each token also carries a tick hash: the SHA3-256 (NIST SHA3, not Keccak) hex digest of the lowercased ticker. Indexers use it as a stable key for tickers containing characters that are awkward in a database column.

  • Max ticker length5Unicode code points
  • Case handlinglowercasedbefore every comparison
  • Tick hashSHA3-256of the lowercased ticker
Difference from BRC-20

BRC-20 tickers are exactly 4 bytes. SRC-20 tickers are 1 to 5 code points from a restricted character set. A valid BRC-20 ticker is usually a valid SRC-20 ticker, but the reverse frequently is not, and a 5-character SRC-20 ticker has no BRC-20 equivalent.


8. Numeric rules

  • N-1

    The value of max, lim and amt must satisfy 0 ≤ value ≤ 18446744073709551615, that is, the unsigned 64-bit maximum. A value outside the range, or a NaN, excludes the transaction.

  • N-2

    Numeric text must match ^[0-9]*(\.[0-9]*)?$. A leading sign, a thousands separator, whitespace, or any other character fails the match and sets the field to null with an invalid-number status.

  • N-3

    max and lim are truncated to integers, rounding down. A deploy declaring "lim": "1000.9" deploys a mint limit of 1000. This truncation is applied to max and lim only.

  • N-4

    amt is not truncated. It keeps its fractional part and is checked against the token's declared decimals instead. This asymmetry is real and is a common source of implementation divergence.

  • N-5

    dec must match ^[0-9]+$ and lie in 0 to 18 inclusive. A value outside that range, or non-integer text, sets dec to null with an invalid-decimal status. When a DEPLOY omits dec, the token's decimals default to 18.

  • N-6

    Below height 833000 only, a numeric supplied as a string had every character that was not a digit or a dot removed before parsing. "21,000,000" became 21000000, and "1 000" became 1000. At and above 833000 the string is parsed strictly and a malformed value excludes the transaction. Historical blocks must be replayed with the old behaviour.

  • N-7

    For MINT and TRANSFER, the number of decimal places in amt after normalisation must not exceed the token's dec. If it does, the operation is invalid with an invalid-decimal status. Trailing zeros do not count, because the value is normalised first: 1.500 against a token with dec of 1 is fine, because it normalises to 1.5.

  • Numeric ceiling18446744073709551615264 minus 1, for max, lim and amt
  • Decimals range0 – 18default 18 when omitted
  • RoundingROUND_DOWNmax and lim only, never amt
Difference from BRC-20

BRC-20 defaults dec to 18 as well, but its ceiling is uint64_max expressed differently and its reference indexers reject fractional max/lim outright rather than truncating them. If you port a BRC-20 validator to SRC-20 without changing the rounding behaviour, deploys with fractional limits will disagree.


9. Operations

Three operations are recognised. In each table, "required" means the key must be present for the payload to be considered that operation's shape at all.

9.1 DEPLOY

Creates a token. Required key set: op, tick, max, lim.

DEPLOY fields
FieldRequiredTypeRules
pYesstringLowercased must equal src-20. See P-3.
opYesstringUppercased must equal DEPLOY.
tickYesstringRules T-1 to T-5.
maxYesstring or numberTotal supply. Truncated down to an integer (N-3), range N-1.
limYesstring or numberMaximum per single mint. Truncated down (N-3), range N-1.
decNostring or numberDecimal places, 0 to 18. Defaults to 18 when absent (N-5).
descNostringStored as token metadata. No protocol effect.
xNostringMetadata: X / Twitter handle. No protocol effect.
webNostringMetadata: website. No protocol effect.
emailNostringMetadata: contact email. No protocol effect.
tgNostringMetadata: Telegram. No protocol effect.

Metadata fields are written to a separate table and later deploys of the same ticker cannot overwrite them with nulls. They do not participate in validity.

9.2 MINT

Issues supply to the transaction's destination address. Required key set: op, tick, amt.

MINT fields
FieldRequiredTypeRules
pYesstringLowercased must equal src-20.
opYesstringUppercased must equal MINT.
tickYesstringMust name an already deployed token (V-4).
amtYesstring or numberAmount to mint. Not truncated (N-4). Decimal places checked against dec (N-7). May be clamped, see V-6 and V-7.

The minted balance is credited to output 0's address, not to the sender. Minting to someone else is therefore expressed simply by paying output 0 to them.

9.3 TRANSFER

Moves balance from the sender to the destination. Required key set: op, tick, amt.

TRANSFER fields
FieldRequiredTypeRules
pYesstringLowercased must equal src-20.
opYesstringUppercased must equal TRANSFER.
tickYesstringMust name an already deployed token (V-4).
amtYesstring or numberAmount to move. Never clamped. If the sender's balance is short, the whole transfer is invalid (V-9).
BULK_XFER is not live

The reference indexer contains a handle_bulk_transfer function and a BULK_XFER key set (op, tick, amt, destinations) that would airdrop an amount to every holder of another ticker. It is unreachable. Operation dispatch matches only DEPLOY, MINT and TRANSFER; every other value, BULK_XFER included, falls to the unsupported-operation branch and is marked invalid. Do not implement it, and do not broadcast it.


10. Validity and state transitions

Plate IV · Token lifecycle

SRC-20 token lifecycle A deploy creates the token and fixes max, lim and dec. Mints add to total minted and credit output zero's address, and are clamped to whichever of the remaining supply or the per-mint limit is smaller. When total minted reaches max, further mints are over-mint and invalid. Transfers move balance between addresses and are rejected outright when the sender's balance is insufficient. DEPLOY fixes max, lim, dec first one wins MINT credits output 0 amt clamped, still valid TRANSFER sender → output 0 never clamped total minted ≥ max → OM, invalid balance < amt → BB, invalid, no partial fill Order within a block is transaction order. Balances update as the block is processed.
Plate IV. The asymmetry between MINT and TRANSFER is the single most important behavioural rule in the protocol: an over-large mint is trimmed and still counts, while an over-large transfer is thrown away entirely.

10.1 DEPLOY

  • V-1

    A DEPLOY is valid when the ticker has no existing deploy and both max and lim are present and non-zero after processing.

  • V-2

    First deploy wins. Any later DEPLOY of the same ticker is invalid with status DE. There is no re-deploy, no transfer of deploy authority, and no way to change max, lim or dec afterwards.

  • V-3

    Within a single block, transactions are processed in block order, so the earlier transaction in the block takes the ticker.

10.2 MINT

  • V-4

    A MINT for a ticker with no deploy is invalid with status ND.

  • V-5

    If total minted is already greater than or equal to max, the mint is invalid with status OM. The token is finished.

  • V-6

    If amt exceeds the remaining supply, it is reduced to exactly the remaining supply and the mint remains VALID, with status OMA recording the reduction. This is how the final mint of a token behaves: whoever mints last receives only the dust that is left, not nothing.

  • V-7

    If amt exceeds the effective per-mint limit, it is reduced to that limit and the mint remains VALID, with status ODL. The effective limit is min(lim, max): a deploy whose lim is larger than its max silently behaves as though lim equalled max.

  • V-8

    Both clamps can apply to one mint, the remaining-supply clamp first and then the per-mint clamp. The credited amount is the one that survives both.

The clamping gotcha

OMA and ODL are not failure statuses. In the reference implementation each status carries an is_invalid flag, and for these two it is false. A validator that treats "amount was reduced" as "operation rejected" will under-count supply and diverge from every other indexer. If you take one rule from this page, take this one.

10.3 TRANSFER

  • V-9

    A TRANSFER is valid only when the sender's running balance is greater than or equal to amt at the moment the operation is processed. Otherwise it is invalid with status BB and nothing moves. There is no partial transfer.

  • V-10

    A TRANSFER for a ticker with no deploy is invalid with status ND.

  • V-11

    A self-transfer, where the creator and the destination are the same address, is handled explicitly and is valid if the balance check passes. It is a no-op on the balance.

  • V-12

    "Running balance" includes the effect of earlier valid operations in the same block. Two transfers of the full balance in one block cannot both succeed; the second sees a zero balance and fails with BB.


11. Invalidity conditions

The reference indexer records a status code on every processed operation. The table is reproduced exactly, including which codes leave the operation valid.

Status codes from Src20Processor.STATUS_MESSAGES
CodeMeaningApplies toOperation invalid?
DEDeploy already exists for this tickerDEPLOYYes
NDNo deploy exists for this tickerMINT, TRANSFERYes
OMOver mint: total minted already at or above maxMINTYes
NAMissing or empty amountMINT, TRANSFERYes
OMAAmount reduced to the remaining supplyMINTNo, still valid
ODLAmount reduced to the per-mint limitMINTNo, still valid
BBBalance below the transfer amountTRANSFERYes
UOUnsupported operationany other opYes
IDAmount has more decimal places than the token's decMINT, TRANSFERYes
NNNumeric field failed its regex, or dec out of rangeanyYes

11.1 Exclusion versus invalidity

These are different outcomes and the distinction matters when reconciling two indexers.

Excluded
The transaction never becomes an SRC-20 record at all. It is not listed, not counted, and carries no status. Exclusion happens when the carrier fails to decode, the prefix is missing, the JSON does not parse, scientific notation is present, the ticker fails T-1 to T-3, or a required numeric is out of range or unparseable.
Invalid
The transaction is recognised as an SRC-20 operation and recorded, but it does not change any balance. It appears in activity feeds with its status code. Everything in the table above with "Yes" in the last column is in this category.
Valid
The operation changes state. Note that OMA and ODL operations are valid and do change state, just by less than the payload asked for.

11.2 Carrier-level exclusions

  • X-1

    Multisig-carried SRC-20 without keyburn set to 1 is not an SRC-20 transaction (E-2).

  • X-2

    A transaction whose declared length exceeds the data actually present fails decoding (E-6).

  • X-3

    A transaction carrying a qualifying P2WSH data output whose payload fails the prefix or length check is excluded, and the multisig branch is not tried (E-12).

  • X-4

    A Counterparty-encoded SRC-20 operation at or after height 796000 is ignored (see section 12).


12. The Counterparty dependency

SRC-20 is often described as "built on Counterparty". That was true for roughly 8000 blocks and has not been true since. The accurate account is:

  • CP-1

    From height 788041, SRC-20 operations were carried inside Counterparty asset issuances. For such a transaction to count, the indexer required a Counterparty asset id (cpid) that begins with A, meaning a numeric asset rather than a named one, and a declared supply of 0.

  • CP-2

    From height 793068, SRC-20 operations began appearing directly on Bitcoin with no Counterparty encoding at all.

  • CP-3

    From height 796000, Counterparty-encoded SRC-20 is ignored entirely. A transaction only counts from this height onward if it has no Counterparty asset id. In code, validity is valid_cp_src20() or (is_src20() and not cpid), and valid_cp_src20() itself requires block_index < 796000.

  • CP-4

    An indexer built today still needs Counterparty, but only to replay history below 796000 and to index classic Stamps, which remain Counterparty assets. SRC-20 state from 796000 onward can be derived from Bitcoin alone.

Say it accurately

"SRC-20 is encoded through the Counterparty data layer" describes blocks 788041 to 795999. For every block since, SRC-20 is a Bitcoin-native protocol that shares a data carrier with Bitcoin Stamps and needs no Counterparty parsing. Both halves of that sentence matter: the history is genuinely Counterparty, and the present genuinely is not.