DUST-20 documentation
Test vectors
Valid and invalid cases with expected outcomes and the rule each one exercises.
- 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
C.1 Vector summary
These vectors are the fixtures the decoder runs, the cases the Node test suite asserts, and the content published at conformance.json. Each names the exact issue code a conforming reader must produce, so a documentation claim and a running validator cannot drift apart.
- Total vectors
- 31Published at conformance.json
- Expected accept
- 5Payloads a reader must index
- Expected reject
- 26Each names the exact issue code
- Groups
- Deploy, Mint, ReaderDeploy, mint and reader shape
How to use these
Feed each input to your reader with the stated context. A vector marked ACCEPT must be indexed. A vector marked REJECT must be refused with that exact issue code, not with a different code and not with a warning. If your reader disagrees with a case, the case names the numbered rule so you can read the paragraph that defines the behaviour. Run them all in your browser.
C.2 The vectors
Deploy
ACCEPTValid deploydeploy-validDUST-5.1VERIFIED
Every field is a strict decimal integer string and max_sats equals supply × unit_sats.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000000",
"unit_sats": "546",
"max_sats": "546000000",
"lim_sats": "54600"
}
ACCEPTValid deploy with lim_sats omitteddeploy-no-limitDUST-5.6VERIFIED
lim_sats is optional. Omitting it means there is no per-mint cap.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "grain",
"supply": "21000",
"unit_sats": "1000",
"max_sats": "21000000"
}
ACCEPTlim_sats of "0" means no capdeploy-zero-limitDUST-3.2UNIVERSE
Zero is explicitly "no limit", not a cap of zero satoshis.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "grain",
"supply": "21000",
"unit_sats": "1000",
"max_sats": "21000000",
"lim_sats": "0"
}
REJECT max_sats_mismatchArithmetic mismatch in max_satsdeploy-arithmetic-mismatchDUST-5.4VERIFIED
max_sats must equal supply × unit_sats exactly. Being one satoshi low usually means the builder used floating-point arithmetic.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000000",
"unit_sats": "546",
"max_sats": "545999999",
"lim_sats": "54600"
}
REJECT missing_fieldDeploy missing max_satsdeploy-missing-max-satsDUST-5.1UNIVERSE
max_sats is required. Earlier documentation reported this as an unreconciled compatibility split; the current implementation rejects the payload rather than deriving the value.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000000",
"unit_sats": "546",
"lim_sats": "54600"
}
REJECT unknown_fieldDeploy carrying an unknown fielddeploy-unknown-fieldDUST-5.1UNIVERSE
Operations declare exact key sets. An unknown field is rejected, not ignored, so one payload cannot be read two ways.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000000",
"unit_sats": "546",
"max_sats": "546000000",
"dec": "0"
}
REJECT bad_integerMalformed integer with a leading zerodeploy-leading-zeroDUST-3.1UNIVERSE
Integers are strict decimal strings. "0546" is not in strict form even though it parses.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "0546",
"unit_sats": "546",
"max_sats": "298116"
}
REJECT bad_integerFractional unit_satsdeploy-floatDUST-3.1VERIFIED
Satoshis are indivisible. Decimal points are rejected outright.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000",
"unit_sats": "546.5",
"max_sats": "546500"
}
REJECT bad_integerZero supplydeploy-zero-supplyDUST-3.1VERIFIED
supply must be a positive integer. Zero and negative values are rejected.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "0",
"unit_sats": "546",
"max_sats": "0"
}
REJECT bad_integerNegative unit_satsdeploy-negativeDUST-3.1VERIFIED
A minus sign is not part of a strict DUST-20 integer.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000",
"unit_sats": "-546",
"max_sats": "546000"
}
REJECT exceeds_money_supplyBacking exceeds Bitcoin’s monetary supplydeploy-exceeds-moneyDUST-5.5UNIVERSE
max_sats of 10,000,000,000,000,000 exceeds the 2,100,000,000,000,000 satoshis that will ever exist. The arithmetic is self-consistent but unbackable.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "huge",
"supply": "1000000000",
"unit_sats": "10000000",
"max_sats": "10000000000000000"
}
REJECT lim_sats_above_maxlim_sats greater than max_satsdeploy-limit-above-maxDUST-5.6UNIVERSE
A per-mint cap larger than the entire backing is contradictory.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "dust",
"supply": "1000",
"unit_sats": "546",
"max_sats": "546000",
"lim_sats": "546001"
}
REJECT bad_tickTicker containing whitespacedeploy-tick-whitespaceDUST-4.4UNIVERSE
Tickers exclude whitespace, control characters and the URL delimiters / ? # \ so an identity is always addressable.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "my dust",
"supply": "1000",
"unit_sats": "546",
"max_sats": "546000"
}
REJECT duplicate_deploymentRedeploying an existing tickerdeploy-duplicate-tickerDUST-5.7UNIVERSE
Identity is the NFC form folded to lower case, so "DUST" is the same ticker as "dust". The first valid deployment wins.
Input
{
"p": "dust-20",
"op": "deploy",
"tick": "DUST",
"supply": "5000",
"unit_sats": "546",
"max_sats": "2730000"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
}
}
Mint
ACCEPTValid mint with matching outputmint-validDUST-6.7VERIFIED
sats equals amt × unit_sats, and the observed output value equals sats.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "100",
"sats": "54600"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
},
"outputValueSats": "54600"
}
ACCEPTMint naming the ticker in a different casemint-case-insensitive-tickDUST-4.5UNIVERSE
The mint resolves against the folded identity, so casing does not prevent resolution.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "DuSt",
"amt": "1",
"sats": "546"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
},
"outputValueSats": "546"
}
REJECT mint_sats_mismatchDeclared sats do not match the deployment ratiomint-sats-mismatchDUST-6.4VERIFIED
100 units at 546 satoshis is 54,600, not 54,000.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "100",
"sats": "54000"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
},
"outputValueSats": "54000"
}
REJECT output_value_mismatchOutput value differs from declared sats by one satoshimint-output-mismatchDUST-6.7VERIFIED
The payload is internally consistent, but the real Bitcoin output holds 54,599 satoshis. The mint is invalid and cannot be repaired after broadcast.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "100",
"sats": "54600"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
},
"outputValueSats": "54599"
}
REJECT mint_exceeds_limitMint exceeds lim_satsmint-limit-violationDUST-6.5VERIFIED
55,146 satoshis exceeds the declared per-mint cap of 54,600.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "101",
"sats": "55146"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
},
"outputValueSats": "55146"
}
REJECT mint_exceeds_supplyMint exceeds remaining supplymint-exceeds-supplyDUST-6.6VERIFIED
999,950 units are already minted, so only 50 remain. The mint is rejected in full rather than partially filled.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "100",
"sats": "54600"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "0",
"minted": "999950"
},
"outputValueSats": "54600"
}
REJECT deployment_missingMint for a ticker that was never deployedmint-no-deploymentDUST-6.2VERIFIED
A mint must resolve exactly one accepted deployment. Without it there is no unit size to check against.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "ghost",
"amt": "10",
"sats": "5460"
}
REJECT missing_fieldMint missing the sats fieldmint-missing-fieldDUST-6.1VERIFIED
sats is required. It is never inferred from amt, even though the ratio is known.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "100"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
}
}
REJECT bad_integerMint of zero unitsmint-zero-amountDUST-6.3VERIFIED
amt must be a positive integer.
Input
{
"p": "dust-20",
"op": "mint",
"tick": "dust",
"amt": "0",
"sats": "0"
}
Context
{
"deployment": {
"tick": "dust",
"display": "DUST",
"supply": "1000000",
"unitSats": "546",
"maxSats": "546000000",
"limSats": "54600",
"minted": "0"
}
}
Reader
REJECT bad_operationUnknown operationunknown-operationDUST-4.2UNIVERSE
Only deploy and mint exist. Any other op is rejected.
Input
{
"p": "dust-20",
"op": "update",
"tick": "dust",
"amt": "1"
}
REJECT bad_operationA transfer inscriptiontransfer-inscriptionDUST-7.1UNIVERSE
No transfer inscription is valid. Movement happens by spending the satoshis that carry the units. Earlier documentation listed an official transfer payload as an open question; the current implementation answers it by accepting none.
Input
{
"p": "dust-20",
"op": "transfer",
"tick": "dust",
"amt": "10"
}
REJECT bad_protocolA different protocol identifierwrong-protocolDUST-4.1VERIFIED
Content whose p is not dust-20 is not a DUST-20 message.
Input
{
"p": "brc-20",
"op": "deploy",
"tick": "dust",
"max": "1000",
"lim": "10"
}
REJECT not_flat_stringsA numeric JSON valuenon-string-valueDUST-2.5UNIVERSE
Every value must be a JSON string. A bare number is rejected because JSON numbers cannot carry large integers exactly.
Input
{"p":"dust-20","op":"mint","tick":"dust","amt":100,"sats":"54600"}
REJECT duplicate_keyA repeated fieldduplicate-keyDUST-2.6UNIVERSE
A duplicate key is rejected rather than resolved last-wins, because two readers would otherwise disagree about the amount.
Input
{"p":"dust-20","op":"mint","tick":"dust","amt":"100","amt":"1","sats":"54600"}
REJECT trailing_contentTrailing content after the objecttrailing-contentDUST-2.4UNIVERSE
Content after the closing brace makes the message ambiguous and is rejected.
Input
{"p":"dust-20","op":"mint","tick":"dust","amt":"1","sats":"546"} extra
REJECT not_flat_stringsA nested objectnested-objectDUST-2.5UNIVERSE
A payload is a flat object of strings. Nesting is rejected.
Input
{"p":"dust-20","op":"deploy","tick":"dust","meta":{"x":"1"}}
REJECT not_objectContent that is not a JSON objectnot-jsonDUST-2.4UNIVERSE
Inscription content must parse as a single JSON object.
Input
dust-20 deploy dust 1000
C.3 Transaction scenarios
Payload vectors check messages. These check constructions: the same ordinal flow rule applied to whole transactions, with the outcome each one produces.
| Scenario | Construction | Outcome | Rule |
|---|---|---|---|
| Partial send with colored changeSend 10 of 91 units and keep the rest. The reference safe construction. | colored + cardinal → colored + colored + cardinal |
NO LOSS | DUST-7.7 |
| Send the whole allocationNothing remains, so no colored change output is needed. | colored + cardinal → colored + cardinal |
NO LOSS | DUST-7.4 |
| Missing colored change burns the remainderOnly the receiver output is colored, so the other 81 units are destroyed. | colored + cardinal → colored + cardinal |
BURNS | DUST-8.3 |
| Paying the fee out of colored backingNo cardinal input, so the fee eats into the colored range and burns units. | colored → colored + colored |
BURNS | DUST-8.1 |
| An output the reader cannot attributeUnits landing in an output with no supported address are destroyed. | colored + cardinal → unsupported + colored + cardinal |
BURNS | DUST-8.2 |
| Merging two allocationsTwo colored inputs of the same ticker combine into one output. | colored + colored + cardinal → colored + cardinal |
NO LOSS | DUST-7.6 |
Each scenario runs in the allocation simulator, which reports the exact unit landings and burn totals rather than a summary verdict.
C.4 Issue codes
The stable code set a reader emits. Codes are part of the contract: two implementations that reject the same payload should reject it for the same stated reason.
| 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. |