BRC-20

A fungible token ledger written in Bitcoin inscriptions

Conformance

Test vectors

Two sets. Payload vectors are decided from the JSON alone, so a parser can be tested against them with no chain state. Ledger vectors are sequences of operations with the exact resulting balances, which is where implementations actually diverge. Every expected outcome cites the rule that produces it.

How to use these

Paste any payload vector into the validator to see the same verdict rendered check by check. The ledger vectors are stated as postings so they can be compared against an indexer's own event stream. Addresses are written as A, B, and C to keep the arithmetic readable; a real implementation keys balances by output script (R22).

1. Payload vectors

Each vector is decided by payload rules only. "Valid" here means the payload is well-formed; whether it takes effect still depends on chain state, which the ledger vectors cover.

1.1 Valid payloads

V1 · deploy, classicvalid
{"p":"brc-20","op":"deploy","tick":"ordi","max":"21000000","lim":"1000"}

The original ordi deploy. Four-byte ticker, positive max, lim at or below max, dec omitted so divisibility is 18 (R7, R9).

V2 · deploy with explicit decimalsvalid
{"p":"brc-20","op":"deploy","tick":"tst4","max":"1000","lim":"10","dec":"2"}

dec of 2 means amounts for this ticker may carry at most two decimal places. lim of 10 caps each mint; 100 mints are needed to exhaust the supply (R9).

V3 · mintvalid
{"p":"brc-20","op":"mint","tick":"ordi","amt":"1000"}

All required fields present as strings. Effect depends on the deploy's lim and remaining supply (R13, R14).

V4 · transfervalid
{"p":"brc-20","op":"transfer","tick":"ordi","amt":"100"}

Well-formed. Validity at confirmation also requires 100 available at the holding address (R16).

V5 · self-mint deploy, 5-byte tickervalid
{"p":"brc-20","op":"deploy","tick":"gamma","max":"1000","self_mint":"true"}

gamma is 5 UTF-8 bytes, permitted only because self_mint is the string "true". Omitting lim means the per-mint limit equals max (extensions).

V6 · formatting and unknown keysvalid
{
  "p": "brc-20",
  "op": "mint",
  "tick": "ORDI",
  "amt": "1.500",
  "memo": "ignored"
}

Whitespace is irrelevant. "ORDI" is the same ticker as ordi (R8). "1.500" trims to one significant decimal place, so it fits any dec of 1 or more (R11). Unknown keys are ignored (R6).

1.2 Invalid payloads

I1 · numeric field as a JSON numberinvalid
{"p":"brc-20","op":"deploy","tick":"ordi","max":21000000,"lim":"1000"}

The single most common mistake. max is an unquoted JSON number; every BRC-20 value must be a JSON string. Write "max":"21000000" (R3).

I2 · wrong protocol tagnot BRC-20
{"p":"brc20","op":"mint","tick":"ordi","amt":"10"}

p must be exactly "brc-20", with the hyphen. No BRC-20 indexer reads this inscription at all (R4).

I3 · operation not lowercaseinvalid
{"p":"brc-20","op":"Mint","tick":"ordi","amt":"10"}

op values are case-sensitive and lowercase. Ticker comparison is case-insensitive, but nothing else is (R5, R8).

I4 · ticker too shortinvalid
{"p":"brc-20","op":"mint","tick":"ord","amt":"10"}

Three UTF-8 bytes. A ticker is exactly 4 bytes, or 5 under the self-mint extension (R7).

I5 · 5-byte ticker without self-mintinvalid
{"p":"brc-20","op":"deploy","tick":"gamma","max":"1000"}

Same ticker as V5 but no "self_mint":"true", so the 5-byte length is not permitted (R7).

I6 · decimals above the maximuminvalid
{"p":"brc-20","op":"deploy","tick":"dec9","max":"1000","dec":"20"}

Divisibility is capped at 18 decimal places (R9).

I7 · per-mint limit above maximum supplyinvalid
{"p":"brc-20","op":"deploy","tick":"lim9","max":"1000","lim":"5000"}

lim may not exceed max (R9).

I8 · zero amountinvalid
{"p":"brc-20","op":"mint","tick":"ordi","amt":"0"}

Amounts must be positive. Zero-value operations are not a way to signal anything (R9).

I9 · too many decimal placesinvalid
{"p":"brc-20","op":"mint","tick":"ordi","amt":"1.0000000000000000001"}

Nineteen fractional digits, above the protocol maximum of 18. Against a ticker with a smaller dec, correspondingly fewer digits are allowed (R11).

I10 · value above uint64invalid
{"p":"brc-20","op":"deploy","tick":"big4","max":"18446744073709551616"}

One above the uint64 maximum, 18446744073709551615. Indexers apply uint-safe arithmetic (R9).

I11 · leading zerosinvalid
{"p":"brc-20","op":"transfer","tick":"ordi","amt":"0100"}

The amount grammar forbids leading zeros. Write "100" (R11).

I12 · missing required fieldinvalid
{"p":"brc-20","op":"mint","tick":"ordi"}

A mint requires amt. There is no default (R13).

I13 · malformed JSONnot BRC-20
{"p":"brc-20","op":"mint","tick":"ordi","amt":"10",}

Trailing comma. The content does not parse as JSON, so it is not an operation at all (R1).

I14 · array instead of objectnot BRC-20
[{"p":"brc-20","op":"mint","tick":"ordi","amt":"10"}]

An operation is one JSON object. Batching several operations into one inscription is not part of the protocol (R1).

2. Ledger vectors

These fix the behavior that payload checking cannot: ordering, supply accounting, the two-phase transfer, and reversal. Each vector lists operations in block order and the exact resulting state.

L1 · First is valid

Operations
BlockOperationOutcome
900001{"p":"brc-20","op":"deploy","tick":"tst4","max":"1000","lim":"10"}Applied. tst4 is now deployed with max 1000, lim 10.
900002{"p":"brc-20","op":"deploy","tick":"TST4","max":"9999999","lim":"9999999"}Ignored. Same ticker after lowercasing.
900003{"p":"brc-20","op":"mint","tick":"tst4","amt":"10"} by AApplied against the first deploy: 10 is within lim 10.
900003{"p":"brc-20","op":"mint","tick":"tst4","amt":"5000"} by AInvalid. 5000 exceeds lim 10 from the owning deploy.

Expected state: tst4 has max 1000, lim 10, dec 18, minted supply 10. A holds 10 available. The second deploy never existed as far as the ledger is concerned (R10, R8).

L2 · The mint remainder rule

Ticker rem4 deployed with max 1000 and lim 400.

Operations, in block order
#OperationCreditedMinted supply
1A mints 400400400
2B mints 400400800
3C mints 4002001000
4A mints 101000

Expected state: A holds 400, B holds 400, C holds 200. Operation 3 is credited only the 200 remaining, not the 400 requested, and not rejected. Operation 4 is invalid because the supply is exhausted (R14).

Trap: an implementation that rejects operation 3 outright, or credits it the full 400, diverges from every other indexer from that block onward.

L3 · Two-phase transfer, all three outcomes

A holds 1000 ordi, all available. Each row starts from that state.

Postings per outcome
Sequence A available A transferable B available
Start100000
A inscribes transfer of 1009001000
…then spends it to B9000100
…or spends it back to A100000
…or spends it as fee100000
…then moves the spent inscription againNo further BRC-20 effect, whatever it does.

Expected behavior: the inscribe step never changes A's overall balance; it only reclassifies 100 from available to transferable. Self and fee spends both restore availability and credit nobody. The second and later spends of the same inscription are inert (R17 to R21).

Trap: collapsing inscribe and spend into one event produces the right final balances for the recipient case and the wrong ones for self and fee, and it breaks state hashes against implementations that emit both events.

L4 · Over-reservation and permanent voiding

Operations, in block order
BlockOperationOutcome
900010A holds 100 available; A inscribes transfer of 60Valid. A: 40 available, 60 transferable.
900011A inscribes transfer of 60 againVoid. Available balance is 40, not 100.
900012A receives 500 more ordiA: 540 available, 60 transferable.
900013A spends the block 900011 inscription to BNo effect. It was void at confirmation and stays void.
900014A spends the block 900010 inscription to BSettles: A 540 overall, B credited 60.

Expected state: A holds 540 available and 0 transferable; B holds 60 available. The later balance increase never revives the void inscription (R16).

L5 · Reorg reversal

Chain event sequence
StepEventExpected state
1Block 900001: A mints 1000 tst4A: 1000 available.
2Block 900002: A inscribes transfer of 250, then spends it to BA: 750 overall; B: 250 available.
3Blocks 900001 and 900002 are orphanedBoth blocks' events are reversed: A and B hold nothing, minted supply returns to 0.
4Replacement block 900001' contains only the mintA: 1000 available; B: 0. The transfer never happened.

Expected behavior: the state after step 4 must be identical to a clean replay of the new chain. Reversal must restore both sides of the transfer and return the reservation, and reverting a deploy must revert every mint and transfer that depended on it.

Trap: reversing only the settlement and leaving the reservation behind leaves A with a phantom transferable balance that no clean replay would produce. Reversal below a declared finality depth should be refused as an error rather than applied.

3. Where these come from

Payload vectors follow the rules in the specification as published by domo and maintained by the Layer1 Foundation, with OPI as the reference implementation. Ledger vectors are stated to match the behavior the Bitcoin Universe BRC-20 read model enforces: per-mint limit and maximum supply invariants, immutable deploy economics, case-insensitive ticker identity with preserved display casing, separate transfer-inscribe and first-spend events with recipient, self, and fee dispositions, integer-only arithmetic on scaled amounts, and transactional reorg reversal that refuses to cross the finalized height. Behavior that is not implemented in the org's own code is not asserted here.