Normative · Bitcoin mainnet · btc_stamps 1.9.3
SRC-101 specification
Every rule below states what the reference indexer does, and names the file that does it. Where the code contains a branch that cannot be reached, or a field that is declared and never used, this document says so rather than describing the intention.
Rules are numbered once and never renumbered. If a rule is withdrawn its number is retired and the changelog records why. A rule marked Observation is not normative: it describes behaviour of the reference implementation that an independent implementer must reproduce to stay in consensus, but which reads as a defect rather than a design.
Contents
1. Scope and terminology
SRC-101 is defined on Bitcoin mainnet only. No other chain and no test network is in scope. Nothing in this document describes behaviour on signet, testnet or any fork.
An operation is a UTF-8 JSON object carried in a Bitcoin transaction through the Bitcoin Stamps data carrier. The five operations are deploy, mint, transfer, setrecord and renew. There is no burn, no revoke, no reclaim, no subdomain, and no batch operation other than the multi-name form of mint.
A namespace is created by one deploy and is identified for all time by the transaction hash of that deploy. Every other operation names its namespace through the hash field. Two namespaces may contain the same name without conflict; a name is only unique within its namespace.
A name is carried in the tokenid field as a Base64 string. Its decoded, lower-cased UTF-8 form is the identity used for every comparison and every database lookup. Two Base64 encodings that decode to the same text are the same name.
A registration is a lease with an absolute end. Each entry carries an expire_timestamp in Unix seconds. The block timestamp of the transaction being processed is compared against it. There is no grace period and no redemption window.
The creator of an operation is the address that funded input 0 of the transaction. The destination is the address of output 0, and the destination value is the satoshi value of output 0. Ownership checks compare against the creator; payment checks compare against the destination value.
2. The data carrier
This section is identical in substance to the SRC-20 carrier. If you have implemented one, you have implemented the other. It is repeated here so that this document stands alone.
The carrier prefix is the six ASCII bytes stamp:. The framed buffer is a two-byte big-endian length, then the prefix, then the JSON. The length counts the prefix and the JSON, and does not count itself.
There are exactly two carriers: bare multisig and P2WSH. OP_RETURN is not an SRC-101 carrier. A transaction containing an OP_RETURN is noted as such and the output is otherwise ignored.
Bare multisig. Every output whose script ends in OP_CHECKMULTISIG contributes its public keys. The data chunk is the concatenation of each key with its first and last byte removed. That chunk is decrypted with ARC4, keyed on the byte-reversed transaction hash of the outpoint spent by input 0. After decryption, bytes 2 through 7 must equal stamp:, and the first two bytes give the length. The payload is the bytes after the prefix up to that length.
P2WSH. Every output at index greater than zero whose script is a 32-byte witness program contributes its 32 bytes. The chunks are concatenated in output order, trailing zero bytes are stripped, the first two bytes give the length, and the data that follows must begin with stamp:. A P2WSH-carried payload has its keyburn forced to 1 by the decoder.
P2WSH chunk collection is gated at block 865000, a constant named for SRC-20 but shared by every Stamps sub-protocol. Below that height there is no P2WSH carriage for anything, SRC-101 included. Since SRC-101 does not begin until block 870652, in practice both carriers are available for the whole life of the protocol.
P2WSH takes precedence over multisig, with no fallback. When a transaction carries qualifying P2WSH data, the multisig branch is not attempted, even when the P2WSH data fails its own length check. The source marks this exclusion as consensus-load-bearing. Adding a fallback forks the ledger.
The destination value is read differently by carrier. On the multisig branch the destination value is always the satoshi value of output 0. On the P2WSH branch it is zero until block 940000, at which height the decoder begins reading output 0. Because mint and renew require a payment measured against that value, a P2WSH-carried mint or renew cannot satisfy its payment check below block 940000 unless the required amount is zero. deploy, transfer and setrecord do not read the destination value and are unaffected.
The payload must decode as UTF-8 and parse as a JSON object. All top-level keys are lower-cased before any further processing, so "P", "OP" and "Hash" are normalised to p, op and hash. JSON floating-point literals are parsed as decimals, not binary floats.
3. Classification and activation heights
A payload is classified as SRC-101 when its lower-cased p value, upper-cased, equals SRC-101. The three sub-protocols the carrier recognises are SRC-721, SRC-20 and SRC-101. Any other value makes the payload not a sub-protocol at all.
A transaction is SRC-101 only when the classification in rule 15 holds and keyburn == 1. This is the same keyburn requirement SRC-20 carries. On the P2WSH branch the decoder sets keyburn to 1 itself, so the requirement is effectively a multisig-branch condition.
SRC-101 is valid only at block 870652 and above. Below that height no SRC-101 payload has any meaning. The constant is BTC_SRC101_GENESIS_BLOCK.
At and above block 872200 the mint key set changes: img leaves the required set and key matching for mint alone relaxes from exact to superset. The constant is BTC_SRC101_IMG_OPTIONAL_BLOCK. Rules 21, 48 and 53 give the detail.
At and above block 940000 the decoder reads output 0's value on the P2WSH branch, as rule 13 describes. The constant is BTC_SRC101_OLGA_BLOCK, annotated in the source as approximately March 2026. This height changes payment visibility, not carrier availability.
| Height | Constant | What changes |
|---|---|---|
| 865000 | BTC_SRC20_OLGA_BLOCK | P2WSH chunk collection begins, shared across sub-protocols. Predates SRC-101 genesis |
| 870652 | BTC_SRC101_GENESIS_BLOCK | SRC-101 becomes valid. Nothing before this height counts |
| 872200 | BTC_SRC101_IMG_OPTIONAL_BLOCK | img becomes optional on mint, and mint key matching relaxes to superset |
| 940000 | BTC_SRC101_OLGA_BLOCK | Output 0's value becomes readable on the P2WSH branch, so P2WSH mint and renew payments can be satisfied |
SRC-20 stops recognising Counterparty-carried operations at block 796000. SRC-101 has no equivalent cut-off. A Counterparty-carried SRC-101 is accepted at any height at or above genesis, and the classification gate does not impose the extra conditions SRC-20 imposes on its Counterparty path. Section 12 states this precisely.
4. The payload and its key sets
Each operation has a fixed key set. For deploy, transfer, setrecord and renew, the payload's key set and the operation's key set are compared with a symmetric difference, and that difference must be empty. Every listed key must be present and no other key may be present. A payload that fails this test is not an SRC-101 operation: it is discarded before any handler runs, leaves no row in any table, and produces no status code.
For mint the comparison depends on height. Below block 872200 it is the same symmetric difference as rule 20, against a set that includes img. At and above 872200 it becomes a containment test: every key in the set must be present, and additional keys are permitted. The set at that height no longer includes img, which is why img becomes optional.
The op value is compared case-insensitively against the five operation names. Any other value causes the payload to be discarded exactly as in rule 20. There is no unsupported-operation record for an unrecognised op at this stage.
| Operation | Keys | Count | Matching |
|---|---|---|---|
deploy |
p, op, root, name, tick, lim, owner, rec, pri, desc, mintstart, mintend, wla, imglp, imgf, idua |
16 | Exact, all heights |
mint, below 872200 |
p, op, hash, toaddress, tokenid, dua, prim, sig, img, coef |
10 | Exact |
mint, 872200 and above |
p, op, hash, toaddress, tokenid, dua, prim, sig, coef |
9 required | Superset, extras allowed |
transfer |
p, op, hash, toaddress, tokenid |
5 | Exact, all heights |
setrecord |
p, op, hash, tokenid, type, data, prim |
7 | Exact, all heights |
renew |
p, op, hash, tokenid, dua |
5 | Exact, all heights |
Because matching is exact, every key in a set is mandatory even when it carries no value. A mint below 872200 that has no whitelist signature must still carry a sig key. The conventional way to satisfy this is the empty string, which the validator normalises to null (rule 27).
A deploy must not carry a hash key. The key set forbids it, and the handler additionally requires that no namespace hash has been resolved.
An operation with a key set naming hash is dispatched only after a namespace lookup succeeds. If hash is absent or does not resolve, the operation is recorded with status IH or ND respectively. ND is one of only two statuses that marks the parse as hard-invalid (rule 75).
The p and op values are upper-cased in the stored record. A row therefore reads SRC-101 and MINT regardless of the case used in the payload.
5. Field rules
Field validation runs before any operation handler. It is fail-closed: see rule 35.
An empty string is normalised to null for every field, before any type-specific rule runs. This is what makes "sig": "" a valid way to satisfy the mandatory-key requirement without supplying a signature.
Numeric fields are lim, dua, idua, mintstart, mintend and coef. Each must match ^[0-9]*(\.[0-9]*)?$ and must coerce to an integer that is at least zero. The stored value is a plain integer, not a decimal. There is no upper bound and scientific notation is rejected by the pattern.
Numeric coercion treats a JSON string and a JSON number differently. The string "1.5" matches the pattern but fails integer conversion, and the operation is discarded. The JSON number 1.5 matches the pattern and converts to 1, silently truncating. Write integers as strings if you want a malformed value to fail loudly.
tokenid is either a Base64 string or a list of Base64 strings. Each string must match ^[A-Za-z0-9+/]+={0,2}$, have a length that is a multiple of four, and be at most 128 characters in encoded form. The decoded text is lower-cased and must not contain a special character (rule 33). Within a list, duplicates after decoding are rejected. The list form is normalised by re-encoding each decoded name with the standard alphabet; the string form is stored as submitted.
The list form of tokenid is decoded with the URL-safe Base64 alphabet while the string form is decoded with the standard alphabet. The acceptance pattern in rule 30 permits only the standard alphabet, so - and _ never reach either decoder. The distinction is therefore invisible in practice, but an implementer copying only the decoder will produce a different result for a name containing those characters.
prim accepts only the two strings "true" and "false". A JSON boolean is not accepted: the comparison is textual. Because a field failure is fatal (rule 35), a payload carrying "prim": true is discarded in full.
root and name, and every decoded tokenid, are rejected if they contain a special character. The test rejects any Unicode character in category Zs (space separators) or Cf (format characters), any all-whitespace value, and any character in a fixed punctuation set that covers ASCII punctuation and a range of CJK full-width punctuation. Ordinary letters, digits and most symbols pass.
The remaining typed fields:
| Field | Rule |
|---|---|
hash | Exactly 64 hexadecimal characters. Stored as the namespace identifier |
owner, toaddress | Must decode as a Bitcoin address, Base58 or Bech32. Bech32 is accepted |
rec | A list whose every member decodes as a Bitcoin address. Stored de-duplicated, and order is not preserved |
wla | A 66-character compressed public key beginning 02 or 03 that is a valid point on secp256k1 |
pri | An object whose keys all parse as integers and are unique, and whose values are all integers. Keys are name lengths, values are prices in satoshis |
imglp, imgf, sig | Must be strings |
img | Must be a list whose members are all strings |
tick | Lower-cased, non-ASCII characters escaped. A SHA3-256 digest of the lower-cased value is stored alongside as tick_hash |
type, data | Stored unchanged, then combined: the value of data is written to a field named for the value of type with _data appended |
desc | Carried through with no validation. Stored as the description |
Field validation is fail-closed. If any field fails its rule, the transaction is abandoned before dispatch. No handler runs, no operation-level status is assigned, and the record carries only the field-level error text. This is the sharpest divergence from SRC-20, where a bad field usually produces an invalid record with a meaningful status code and the operation is still reasoned about.
The source declares two regular expressions named for Bitcoin and Ethereum address validation. Neither is ever referenced. Address validation is delegated to helpers that decode Base58 and Bech32 for Bitcoin, and apply a separate inline pattern for Ethereum. Reading the declared Bitcoin pattern and implementing it would wrongly reject every Bech32 address, since the declared pattern is Base58-only and caps length at 34 characters.
6. deploy
Opens a namespace. It creates no name and claims nothing.
Length limits are enforced on six fields. root, name, tick and imgf must be shorter than 32. imglp must be shorter than 255. pri must have fewer than 255 entries. Failure gives status IDP.
A missing value in any of those six is treated as over-length, because the length test substitutes an over-long placeholder when the field is absent. Combined with the exact key set of rule 20, all six are effectively mandatory and must carry a value.
When mintend is missing or zero it is stored as 18446744073709551615, which is 2^64 minus 1. This is the only place that constant appears in SRC-101, and it functions as "no end".
lim is never enforced as a limit. No handler reads it as a cap on mints, names, or anything else. Its only functional role is as a presence sentinel: the namespace lookup treats a resolved lim of zero as "no such namespace". A namespace deployed with lim zero is permanently unusable, and every later operation against it fails with ND.
mintstart and mintend are compared against the block timestamp, not the block height. They are Unix seconds. The window is half-open: minting requires the block timestamp to be at least mintstart and strictly less than mintend.
The pri object is written to a price table, one row per entry, keyed by name length. The key 0 acts as the fallback price for any length that has no explicit entry. A name whose length has no entry and where no 0 entry exists cannot be priced, and the operation fails with IRL.
rec is written to a recipients table. It is the set of addresses that may collect mint and renew payments; owner is stored but is not used in any payment or authorisation check.
The in-block namespace lookup matches on a field that a deploy row never carries, because a deploy has no hash key and therefore no resolved namespace identifier. Consequently a namespace cannot be resolved from within the block that deployed it, and the database is only written after the block completes. A mint in the same block as its deploy fails with ND. Wait for the deploy to confirm before minting.
7. mint
Claims one or more free names in a namespace for a paid term. The checks below run in the order given; the first failure ends the operation.
The transaction's destination must be a member of the namespace's rec list. A payment to any other address, however large, gives status IR.
tokenid must be a non-empty list for a mint. The single-string form is rejected with ITT. A mint of one name is a list of one.
coef must be an integer in the range 0 to 1000 inclusive. It expresses a discount in parts per thousand. Out of range gives ITC.
Below block 872200, img must be a list. At and above 872200, an absent img is replaced by a list of nulls the same length as tokenid, and a present-but-not-a-list img gives ITI.
dua must be an integer greater than zero, and the namespace's idua must be greater than zero. Failures give ITD and ITID. The submitted dua is then rounded up to the next multiple of idua, and the number of rounds is dua / idua. The rounded value, not the submitted value, is stored and used everywhere afterwards.
The required payment is the sum, over each name being minted, of the price for that name's decoded length multiplied by the number of rounds. Prices come from the namespace's price table, with the 0 entry as fallback.
The whitelist discount. The effective coefficient starts at 1000, meaning full price, and coef is ignored unless proved. If sig is present and non-empty, the indexer verifies an ECDSA signature over SHA-256, against the namespace's wla public key, over the JSON serialisation of one of two payloads, tried in order:
{"hash": <deploy hash>, "coef": "<coef>", "address": <creator>,
"tokenid": <tokenid as submitted>, "dua": "<rounded dua>"}
{"hash": <deploy hash>, "coef": "<coef>", "address": <creator>,
"dua": "<rounded dua>"}
If either verifies, the effective coefficient becomes coef. If neither does, the operation fails with IRS. Note that coef and dua are serialised as strings, that dua is the value after rounding, and that tokenid is the value as submitted rather than the normalised form.
The payment check is destination value >= required payment × effective coefficient / 1000. Failure gives IRV. Overpayment is accepted and is not refunded by the protocol.
When the namespace declares both imglp and imgf, an image URL is derived per name as imglp then the decoded name then . then imgf. Below block 872200 the submitted img entries must equal the derived values exactly, or the operation fails with IRM. At and above 872200 the submitted values are discarded and the derived values are written in their place.
The block timestamp must be at least mintstart (else UT) and strictly less than mintend (else OT).
Names already held with a live expiry are removed from the list and the mint proceeds without them. For each name, the current entry is looked up; if its expiry is later than the block timestamp the name is dropped, and otherwise the current holder is recorded as the new entry's preowner. Only if the list becomes empty does the operation fail, with status DM. There is no refund and no notification for the dropped names.
The new expiry is block timestamp + rounded dua × 31536000. The constant 31536000 is 365 days in seconds, so dua is a count of 365-day years. Leap seconds and leap days are not modelled.
The new owner is the value of toaddress, which need not be the creator. The entry's resolved Bitcoin address is set to that same owner, its resolved Ethereum address is left empty, and its text record is left empty.
8. transfer
The name must already have an entry with an owner and an expiry, else NM. The creator must equal the current owner, else NO. The block timestamp must be strictly less than the expiry, else OE.
tokenid for a transfer is the single-string form. The lookup compares a scalar decoded name; supplying a list produces a lookup that cannot match.
The expiry is carried over unchanged. A transfer does not extend, reset or shorten the term. Buying a name late in its term buys the remainder of that term.
Records do not survive a transfer. The owners row written for a transfer sets the resolved Bitcoin address, the resolved Ethereum address and the text record to empty, and the primary flag to false. The new owner must issue a fresh setrecord to point the name anywhere.
There is no payment check on a transfer. The destination value is not read, and no fee is owed to the namespace.
9. renew
The name must have an entry, the creator must be the current owner, and the block timestamp must be strictly less than the current expiry. Renewing an expired name is refused with OE. This is the rule with the largest practical consequence in the protocol.
dua is validated and rounded exactly as in rule 49. The required payment is the price for the length of the decoded name, with the 0 entry as fallback, multiplied by the number of rounds.
A renew gets no whitelist discount. There is no coef and no sig in the renew key set, and the payment check is a plain destination value >= required payment. A holder who minted at a discount renews at full price.
The new expiry is the existing expiry plus the term, not the block timestamp plus the term. Renewing early loses nothing. The handler contains a branch that would restart the clock from the block timestamp for an already-expired entry, but rule 63 makes that branch unreachable.
Owner, records and primary flag are all carried over unchanged by a renew.
10. setrecord
The name must have an entry, the creator must be the current owner where a creator is present, and the block timestamp must be strictly less than the expiry. The three failures are NM, NO and OE.
The pair type and data is combined into a field named <type>_data. The two the handler understands are address and txt, giving address_data and txt_data. At least one of the two must be present and non-empty, else ID. A type the handler does not understand leaves both empty and therefore also gives ID.
address_data is an object. Its btc member, when present and non-empty, must decode as a Bitcoin address. Its eth member, when present and non-empty, is not an address but a signature: the hexadecimal signature of an Ethereum personal-sign message whose text is the hexadecimal, byte-reversed hash of the transaction spent by input 0. The signer is recovered from that signature, validated as an Ethereum address, and stored with the leading 0x removed.
When prim is true, address_data must be present and its btc member must equal the creator, else IDB. You cannot mark a name primary for an address you are not currently spending from.
Setting the primary flag clears it from every other name that the same resolved Bitcoin address holds in the same namespace. One primary name per address per namespace.
A setrecord that supplies only one of the two record kinds leaves the other as it was. Records are merged, not replaced wholesale. Only a transfer clears them (rule 61).
11. Validity and state
There are three outcomes, and they are not the same thing:
- Valid
- The handler completed and marked the record valid. A row is written to both the operation table and the valid table, and the owners projection is updated.
- Invalid
- The handler ran and assigned a status code. A row is written to the operation table with that status. Nothing changes in the register.
- Excluded
- The payload never became an SRC-101 operation. Failed key matching, an unrecognised
op, a failed field rule, or a failed classification. No row anywhere, no status code.
Only ND and UO mark the parse itself as invalid; every other status is advisory. The block processor discards that flag and keys everything on whether the record was marked valid, so in practice the distinction has no effect on stored state.
Five tables hold SRC-101 state: SRC101 for every parsed operation, SRC101Valid for the valid subset, src101price for deploy prices, recipients for deploy recipient lists, and owners for the current register. The reference describes each.
SRC-101 contributes nothing to the block ledger hash. The hash is computed from valid stamps and the SRC-20 string only. There is no cross-implementation consensus check for SRC-101 comparable to the SRC-20 ledger-hash comparison, so an SRC-101 divergence between two indexers can persist silently.
SRC-101 shares no state with the SRC-20 ledger. No SRC-101 operation reads or writes an SRC-20 balance, and no SRC-20 operation touches a name. A transaction is classified as one protocol or the other, never both.
Within a block, operations are processed in transaction order, and a name minted earlier in a block is visible to a later transaction in the same block. Namespaces are the exception, per rule 44.
There is no mempool state. An unconfirmed transaction has no SRC-101 meaning. Nothing about a name changes until the block containing the operation is processed.
12. The Counterparty relationship
An SRC-101 transaction that also carries a Counterparty asset identifier is accepted, at any height at or above genesis. The classification is a plain disjunction: SRC-101 with an asset identifier, or SRC-101 without one. Both branches are open forever.
The Counterparty branch for SRC-101 imposes none of the extra conditions that the equivalent SRC-20 branch imposes. SRC-20 requires the asset identifier to begin with A, requires a supply of zero, and stops at block 796000. SRC-101 requires only that an asset identifier is present.
An indexer that wants to serve SRC-101 alone still needs Counterparty issuance data available, because the classification gate reads whether an asset identifier was resolved for the transaction. It does not need Counterparty history for SRC-101 state, since no SRC-101 state predates block 870652.
Sources for this document
indexer/src/index_core/src101.pyfor rules 20 to 73 and the status codes.indexer/src/config.pyfor rules 11, 17, 18, 19, 39 and the table names in rule 76.indexer/src/index_core/models.pyfor rules 14, 15, 16, 17, 81 and 82.indexer/src/index_core/transaction_utils.pyfor rules 7 to 13.indexer/src/index_core/database.pyfor rules 42, 43, 44, 76 and the projection described in the reference.indexer/src/index_core/blocks.pyfor rules 75, 77 and 79.indexer/src/index_core/util.pyfor rules 30, 33, 34 and 36.