DUST-20 REV 1.1.0

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.

Transaction constructions and their outcomes
ScenarioConstructionOutcomeRule
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 codes a reader emits, and the rule each one enforces
Issue codeRuleCondition
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.