Normative
Normative specification
Numbered rules, encoding, identifiers, operations and state transitions.
- Protocol
- DUST-20Registry id dust20, alias dust-20
- Chain
- BitcoinNetwork: mainnet
- Ownership model
- UTXOUnits are carried by satoshis in unspent outputs
- Carrier
- Inscription + spendDeploy and mint are inscribed; movement is a spend
- Decimals
- 0Balances are whole units only
- Document version
- 1.1.0Revised 2026-09-01
- Lifecycle
- ExperimentalNot a ratified multi-party standard
- Owning repository
- bitcoinuniverseio/dust-20Documentation source of truth
1 Scope and conventions
This section defines what the following rules cover, how they are numbered, and how to read a provenance label. Sections 2 to 9 are normative. Sections 10 to 12 restate the same rules as tables for implementers.
1.1 Scope
DUST-20 is a fungible token protocol on Bitcoin mainnet. It defines two written messages, deploy and mint, both carried as inscription content, and it defines how token units follow satoshis through ordinary Bitcoin spends. It defines no consensus change, no script template and no new transaction field. Bitcoin nodes do not know DUST-20 exists.
1.2 Rule identifiers
Every rule has a stable identifier of the form DUST-<section>.<index>, for example DUST-6.7. Identifiers do not change between document versions. A rule that is withdrawn keeps its number and is marked withdrawn rather than being reused. Each rule links to the conformance vector that exercises it, where one exists.
1.3 Requirement wording
A rule that says a payload is rejected means a conforming reader must not index it, must not repair it, and must not fall back to a partial interpretation. Rejection is total: there is no warning level, and no rule degrades to "ignore what you do not understand".
1.4 Provenance
DUST-20 is not a ratified multi-party standard. Every rule carries a label saying which source supports it, so an implementer can see the difference between an agreed protocol rule and one production implementation's decision.
| Label | Meaning |
|---|---|
| VERIFIED | Documented in the legacy DUST-20 specification and enforced by the Bitcoin Universe production indexer. Both sources agree. |
| UNIVERSE | Enforced by the Bitcoin Universe production indexer. The legacy specification does not settle this point, so an independent implementation may differ. |
| LEGACY | Stated by legacy DUST-20 documentation. Superseded, narrowed, or not enforced identically by the current implementation. |
| IMPL | The behaviour of one application or service. Useful evidence, but not a protocol rule. Do not rely on it across implementations. |
| UNRESOLVED | No authoritative answer exists. Documented as an open question rather than invented. Stop before an irreversible step. |
1.5 Sources
| Source | What it provides |
|---|---|
| Legacy DUST-20 specification | Documents the deploy and mint payloads and a colored-UTXO transfer example. |
| Bitcoin Universe production indexer | index-dust20 reconstructs colored UTXO ownership from Bitcoin Core and a shared Ord 0.29 instance. |
| Bitcoin Universe capability registry | Records, per protocol, which product surfaces and marketplace actions are actually available. |
| This documentation repository | Source of the published documentation and its conformance fixtures. |
2 Message carrier
DUST-20 defines two written messages, deploy and mint. Both are carried as inscription content on Bitcoin. Movement is not a written message at all: it is the shape of an ordinary spend, covered in section 7.
Deploy and mint are inscription content
A DUST-20 deploy or mint is the content of a Bitcoin inscription. There is no OP_RETURN carrier, no separate script template, and no auxiliary transaction field. A reader obtains authoritative inscription identity, content and location from an inscription index and obtains transaction values and addresses from a Bitcoin node.
Content is UTF-8
Inscription content is decoded as UTF-8. Content that is not well-formed UTF-8 is not a DUST-20 message.
Content larger than 4096 bytes is not a candidate
A reader considers content of at most 4096 bytes. Larger content is not evaluated as DUST-20.
Content is exactly one JSON object
Content must parse as a single JSON object. Nothing may follow the closing brace except insignificant whitespace. Trailing content makes the message ambiguous and is rejected.
Every value is a JSON string
The object is flat and every value is a JSON string. Nested objects, arrays, numbers, booleans and null are rejected. JSON numbers cannot carry values above 2^53 exactly, so a bare number is never accepted for a quantity.
Duplicate keys are rejected
A key that appears more than once is rejected rather than resolved last-wins or first-wins. Two readers must never be able to disagree about which value applied.
Raw control characters are rejected
A raw character below U+0020 inside a JSON string makes the content invalid. Escaped sequences that decode to control characters are still subject to the ticker rules in section 4.
3 Integer encoding
Every quantity in DUST-20 is a decimal string in strict form. This exists so that two implementations reading the same bytes compute the same number, and so that a reader cannot silently accept a value produced by floating-point arithmetic.
Positive quantities match /^[1-9][0-9]*$/
supply, unit_sats, max_sats, amt and sats are strict positive decimal strings. A leading zero, a leading plus or minus, whitespace, a decimal point, a thousands separator and exponent notation are all rejected.
Optional caps match /^(0|[1-9][0-9]*)$/
lim_sats is a strict non-negative decimal string. The single character "0" is valid and means no per-mint limit.
Satoshi fields are bounded by the money supply
unit_sats, max_sats, lim_sats and sats may not exceed 2,100,000,000,000,000 satoshis, the total that will ever exist. A payload can be internally consistent and still exceed this bound, in which case it is rejected.
Unit quantities are bounded by 2^128 - 1
supply and amt may not exceed 340,282,366,920,938,463,463,374,607,431,768,211,455. This is the atomic ceiling the production indexer enforces on token quantities.
Arithmetic is exact integer arithmetic
An implementation must evaluate supply × unit_sats and amt × unit_sats with exact integers. A 64-bit float loses precision above 2^53, which is well inside the satoshi range, and a rounded product produces a rejected payload that looks correct on screen.
4 Identifiers
Three fields identify a message: the protocol tag, the operation, and the ticker. The ticker is the only one with a normalization step, and getting that step wrong is the most common source of disagreement between implementations.
p is the literal string dust-20
Content whose p field is absent or is anything other than the seven bytes dust-20 is not a DUST-20 message. There is no case tolerance, no alias, and no versioned variant of the tag.
op is deploy or mint
Exactly two operation values exist. Any other value, including transfer, is rejected.
tick is 1 to 64 UTF-8 bytes in NFC form
The ticker must already be in Unicode Normalization Form C, and its UTF-8 encoding must be between 1 and 64 bytes. Length is measured in bytes, not code points, so a multi-byte ticker is shorter in characters than the bound suggests.
tick excludes control characters, whitespace and / ? # \
These characters are excluded so a ticker identity is always safe to place in a path or query without escaping, and so two tickers cannot look identical in a log line.
Identity is the NFC form folded to lower case
Two tickers are the same ticker when their NFC forms are equal after lower-case folding. DUST, dust and Dust are one identity. Comparison must happen on the folded identity, never on raw bytes.
The original spelling is preserved for display
An implementation stores the folded identity for comparison and the spelling as written for display. Replacing the display form with the folded form loses information a deployer chose deliberately.
5 Deploy
A deploy creates a ticker and fixes, permanently, how many satoshis stand behind one of its units. Everything a mint or a transfer later computes depends on the numbers written here.
The key set is exact
A deploy contains exactly p, op, tick, supply, unit_sats and max_sats, and may contain lim_sats. A missing required key and an unrecognised extra key both reject the whole payload. Unknown fields are not ignored.
supply is the permanent unit ceiling
supply is the total whole units that may ever be minted for this ticker. It cannot be raised, lowered or reissued by any later message.
unit_sats is the permanent backing ratio
unit_sats is the exact number of satoshis that back one unit, for the entire life of the ticker. There is no default: a deploy that omits unit_sats is rejected, and 546 is an example, not a fallback.
max_sats equals supply × unit_sats
max_sats is a redundancy check, not an independent quantity. A deploy whose max_sats differs from the exact product by even one satoshi is rejected rather than corrected.
max_sats may not exceed the money supply
A deployment whose total backing exceeds 2,100,000,000,000,000 satoshis can never be fully minted, so it is rejected at deploy time rather than at the mint that would cross the line.
lim_sats is an optional per-mint satoshi cap
When absent or "0", one mint may take the entire supply. When present and positive it may not exceed max_sats, and every mint must satisfy sats ≤ lim_sats. The equivalent unit cap is the floor of lim_sats ÷ unit_sats.
The first valid deployment of an identity wins
Once a valid deployment exists for a folded ticker identity, every later deployment of that identity is rejected. Ordering is by block height, then transaction index within the block, then inscription order within the transaction.
There are no decimals
DUST-20 balances have zero decimal places. No decimals field exists, and supplying one rejects the payload under DUST-5.1.
5.x Deploy field set
{"p":"dust-20","op":"deploy","tick":"dust","supply":"1000000","unit_sats":"546","max_sats":"546000000","lim_sats":"54600"}
Required: p, op, tick, supply, unit_sats, max_sats. Optional: lim_sats. Nothing else is permitted.
6 Mint
A mint creates units. It is the only point where a written message and the physical shape of a Bitcoin output have to agree exactly, and the only place where an error cannot be repaired after broadcast.
The key set is exact
A mint contains exactly p, op, tick, amt and sats. There are no optional mint fields.
A mint resolves exactly one accepted deployment
The mint names a ticker; the reader folds it to its identity and finds the accepted deployment for that identity. A mint for an identity with no accepted deployment is rejected. Every ratio used to check the mint comes from the resolved deployment, never from the mint payload.
amt is a positive whole number of units
amt is a strict positive decimal string. A mint of zero units does not exist, and neither does a fractional one: there are no decimals, so the smallest possible mint is one whole unit backed by unit_sats satoshis.
sats equals amt × unit_sats
The satoshis a mint declares are the units it creates multiplied by the resolved deployment ratio. sats is never inferred from amt: omitting it rejects the payload.
sats respects the deployment per-mint cap
When the resolved deployment declares a positive lim_sats, a mint whose sats exceeds it is rejected in full.
Mints stop at the declared supply
Previously minted units plus amt must not exceed supply. There is no partial fill: a mint that would cross the ceiling is rejected in full rather than trimmed to the remainder.
The carrying output holds exactly sats satoshis
Three numbers must agree: the deployment ratio, the sats field in the JSON, and the real value of the Bitcoin output carrying the inscription. A one-satoshi difference invalidates the mint, and the transaction still confirms normally.
The inscription sits at satoshi offset 0 of that output
The mint inscription must begin at the first satoshi of the output that carries it. Padding that shifts the inscription away from offset 0 invalidates the mint.
The carrying output must not already hold an allocation
A mint cannot be layered on top of an output that already carries a DUST-20 allocation. Two allocations claiming the same satoshis would make ownership undecidable.
6.x Mint field set
{"p":"dust-20","op":"mint","tick":"dust","amt":"100","sats":"54600"}
Required: p, op, tick, amt, sats. There are no optional mint fields.
The three numbers that must agree
The deployment ratio (unit_sats), the declared backing (sats) and the real value of the Bitcoin output all have to match. A one-satoshi difference makes the mint invalid, and the transaction still confirms. Nothing can repair it afterwards.
7 Allocation and movement
After a mint, DUST-20 stops using messages. An allocation is a span of satoshis inside an output, and moving units means spending those satoshis. Everything in this section is derived from transaction shape, which is why output order and output size are protocol-significant.
No transfer message exists
DUST-20 uses inscriptions only for deploy and mint. An inscription whose op is transfer is invalid, moves nothing, and must not be treated as a transfer by any implementation.
An allocation is an outpoint, an identity, a ratio, an amount and an offset
The offset is the satoshi position at which the allocation begins inside its output. Without it the next spend cannot be computed, so an implementation that stores only an amount per outpoint is incomplete.
Satoshis map input to output in ordinal first-in-first-out order
Lay every input value end to end in input index order to form one satoshi range, and every output value end to end in output index order to form another. The n-th satoshi of the input range is the n-th satoshi of the output range. Satoshis past the end of the output range are the miner fee.
A unit survives only when its whole block lands in one supported output
Each unit is a contiguous block of unit_sats satoshis inside the allocation span. It survives where that entire block falls inside a single output the reader can attribute to a supported address. A block split across an output boundary survives nowhere.
Output order is protocol-significant
Allocation is positional. Moving an output, changing its value, or inserting one changes which satoshis land where and therefore which units survive. Reordering outputs is never cosmetic.
Allocations merge when identity and ratio match
Several allocations of the same ticker identity can be combined into one output, provided every contributing unit lands whole inside it. Allocations of different identities keep separate records even inside one output, tracked by their own offsets.
A partial send requires an explicit colored change output
Spending an allocation of 91 units and sending 10 requires a second output holding at least 81 × unit_sats satoshis, positioned so each unit fits whole. Omitting that output does not keep the remainder: whole units land in whatever supported output the satoshis reach next, and the rest fall into the fee and are burned.
Miner fees are funded from cardinal inputs
Satoshis that become the fee are past the end of the output range, so any units they carried are destroyed. A builder adds an ordinary bitcoin input to pay the fee and never shaves satoshis off a colored output.
8 Burn
Burning is a consequence of transaction shape, not an instruction. There is no way to write "burn this", and there is no way to undo it once the transaction confirms.
Units paid as fees are burned
Satoshis that fall past the last output become the miner fee. Any whole unit inside that range is destroyed.
Units sent to an unattributable output are burned
When a reader cannot attribute an output to a supported address, units landing in it are destroyed rather than held in an unknown state.
Units split across an output boundary are burned
A unit is atomic. If its block of unit_sats satoshis straddles two outputs, no fraction survives in either.
Burns are recorded in supply accounting
A burn decreases total and circulating supply by the burned amount and increases burned supply by the same amount. Minted supply is unchanged: a burn does not free supply for reminting.
A burn is silent on Bitcoin
The transaction confirms normally. No node, no wallet and no block explorer flags it. Only a DUST-20 reader knows anything was destroyed.
9 Indexer semantics
These rules govern what a reader publishes rather than what the chain contains. They are drawn from the event contract the Bitcoin Universe production indexer enforces on every accepted batch.
Every event carries a stable source event id
An event is identified by an id that does not change between observations of the same chain fact. Downstream consumers deduplicate on it.
A confirmed event carries complete block placement
An event marked confirmed must carry block height, block hash, transaction index and event index. Partial placement is rejected: an event either has a full position in the chain or it is pending.
A pending event carries no block placement
An event marked pending must not carry a height, hash, transaction index, event index or a non-zero confirmation count. Mixing the two states hides whether a balance is settled.
Wallet deltas balance per operation
For a transfer the wallet deltas sum to zero. For a mint they sum to the minted amount, and for a burn to the negative of the burned amount. Each delta must equal its own received minus sent, and an address may appear only once per event.
A deploy carries no economic effect
A deploy event states metadata only. It carries no amount, no sender, no receiver, no wallet deltas and no supply movement. Supply appears when units are minted, not when a ticker is created.
Reorganizations are replayed, not patched
On a reorganization a reader emits invalidations newest first, reverses ledger mutations, restores allocations that had been marked spent, removes allocations the orphaned branch created, and replays the replacement branch from the fork point.
Coverage is declared, not assumed
A reader publishes its coverage as complete, partial or unavailable alongside a checkpoint of network, height, hash, finalized height and observation time. A consumer that ignores the coverage label cannot tell a quiet ticker from a stalled indexer.
Mempool coverage is partial
The production indexer reports partial coverage precisely because it has no exhaustive mempool feed with a stable pending lifecycle and disappearance handling. Unconfirmed DUST-20 state is not a reliable input to an irreversible decision.
10 Transaction anatomy
The same rules, arranged by where they apply in a transaction. Use this when you are checking a construction rather than reading the protocol.
10.1 Mint transaction
| Part | Role | Requirement | Verified against |
|---|---|---|---|
| Input 0 | Commit outpoint | Spends the commit output that carries the inscription envelope. |
Inscription index |
| Witness | Inscription envelope | Holds the content type and the JSON payload. Content is UTF-8, at most 4096 bytes for a DUST-20 candidate. |
Inscription index |
| Output 0 | Carrying output | Value must equal the sats field exactly. The inscription must sit at satoshi offset 0. The output must not already carry an allocation. |
Bitcoin node + inscription index |
| Output 1..n | Cardinal change | Ordinary bitcoin. Carries no units and is not evaluated. |
Bitcoin node |
| Fee | Miner payment | Funded from cardinal value. Satoshis that reach the fee are past the output range and carry nothing. |
Derived |
10.2 Transfer transaction
| Part | Role | Requirement | Verified against |
|---|---|---|---|
| Input 0..i | Colored inputs | Outputs that carry allocations. Each contributes a span at a known offset into the concatenated input range. |
Reader allocation ledger |
| Input i+1..n | Cardinal inputs | Ordinary bitcoin added to fund the fee without disturbing colored spans. |
Bitcoin node |
| Output 0..j | Colored outputs | Sized and ordered so each surviving unit lands whole inside one of them. A partial send needs an explicit colored change output. |
Derived from ordinal flow |
| Output j+1..m | Cardinal outputs | Ordinary change. Units landing here are still tracked if the address is supported, which is usually a construction error. |
Derived from ordinal flow |
| Fee | Miner payment | Any colored satoshi that falls into the fee gap is burned. |
Derived |
11 State transitions
| From | Event | To | Rule | Accounting |
|---|---|---|---|---|
| No ticker | Valid deploy inscription |
Ticker deployed, 0 minted | DUST-5.1 to DUST-5.7 | The identity is claimed permanently. A second deploy of the same identity changes nothing. |
| Ticker deployed | Valid mint inscription in an output of exactly sats satoshis |
Allocation live at that outpoint, offset 0 | DUST-6.1 to DUST-6.9 | Minted supply increases by amt. Total and circulating supply increase by amt. |
| Allocation live | The carrying output is spent, whole units land in supported outputs |
Allocation spent, new allocations live | DUST-7.3, DUST-7.4 | Wallet deltas sum to zero across the event. Supply is unchanged. |
| Allocation live | The carrying output is spent, some units land nowhere whole |
Allocation spent, remainder burned | DUST-8.1 to DUST-8.4 | Total and circulating supply fall by the burned amount. Minted supply is unchanged. |
| Allocation live at confirmed depth | The confirming block is orphaned by a reorganization |
Allocation invalidated, prior state restored, replacement branch replayed | DUST-9.6 | Invalidations are emitted newest first. A balance is only as final as the depth it was confirmed at. |
| Pending event | The transaction is replaced or evicted from the mempool |
No event | DUST-9.3, DUST-9.8 | Coverage is partial, so a pending event may disappear without a stable notification. |
12 Invalidity conditions
Every way a payload or a construction can be rejected, with the issue code a reader emits and the rule that defines it. The decoder emits exactly these codes, and the test vectors assert them.
| Issue code | Rule | Condition |
|---|---|---|
| not_object | DUST-2.4 | Content is not a single JSON object. |
| not_flat_strings | DUST-2.5 | A value is not a JSON string, or the object is nested. |
| duplicate_key | DUST-2.6 | A field appears more than once. |
| trailing_content | DUST-2.4 | Content continues after the closing brace. |
| content_too_large | DUST-2.3 | Content exceeds 4096 bytes and is not evaluated. |
| bad_protocol | DUST-4.1 | p is not the literal string dust-20. |
| bad_operation | DUST-4.2 | op is not deploy or mint. |
| bad_tick | DUST-4.3, DUST-4.4 | The ticker is not NFC, is outside 1 to 64 bytes, or contains an excluded character. |
| missing_field | DUST-5.1, DUST-6.1 | A required field of the operation is absent. |
| unknown_field | DUST-5.1, DUST-6.1 | A field outside the operation key set is present. |
| bad_integer | DUST-3.1, DUST-3.2 | A quantity is not a strict decimal string. |
| exceeds_money_supply | DUST-3.3, DUST-5.5 | A satoshi quantity exceeds the total that will ever exist. |
| max_sats_mismatch | DUST-5.4 | max_sats does not equal supply × unit_sats. |
| lim_sats_above_max | DUST-5.6 | lim_sats exceeds max_sats. |
| duplicate_deployment | DUST-5.7 | The ticker identity already has an accepted deployment. |
| deployment_missing | DUST-6.2 | The mint resolves no accepted deployment. |
| mint_sats_mismatch | DUST-6.4 | sats does not equal amt × unit_sats for the resolved deployment. |
| mint_exceeds_limit | DUST-6.5 | sats exceeds the deployment per-mint cap. |
| mint_exceeds_supply | DUST-6.6 | Minted units plus amt would exceed supply. |
| output_value_mismatch | DUST-6.7 | The carrying output value differs from sats. |
| allocation_overflows_input | DUST-7.2 | A claimed allocation does not fit inside the output it is recorded against. |
| outputs_exceed_inputs | DUST-7.3 | Outputs total more than inputs, so the transaction cannot be built. |