The ZMAP Protocol

The Zcash Memo Asset Protocol — how a shielded note carries an asset.

▫ SupersededVersion 0.3 · 2026-08-27

How the first 2026 collections were made: the id was the artwork’s SHA-256. Kept so older assets stay checkable. In force today: v0.5.

No independent security review yet. Wrong or underspecified? That is a defect in the text: tell us.

Status: DRAFT. Published for implementation and review. Version 0.3 is the first public release; versions 0.1 and 0.2 existed only inside the reference implementation and are documented here for compatibility, not for new use.

No external security review has been performed. Everything in this document handles funds. Implement accordingly.

Version 0.3 (draft)
Date 2026-08-27
Network Zcash mainnet, NU6.3 "Ironwood" and later
Also known as ZRC-721
Reference implementation zecboat-core 0.6.1, zmap.rs
Licence of this document Public. Implement it freely, with or without attribution.

What is stable in 0.3

Implement against these and a future version will not break you without a major bump.

Section Element Stability
§2 Carrier note value — exactly 10,000 zatoshi Stable
§3 JSON memo dialect Stable
§3.2 Legacy prose dialect Frozen — parse it, never emit it
§4 asset_id derivation Stable
§5 Transfer semantics — byte-identical memo reproduction Stable
§7 Proof of Issuance manifest, zecboat-proof-of-issuance/1 Stable
§8 Disclosure tuple (txid, action_index, ock) Stable
§9 Verification procedure Stable
§10 Signed price list Provisional
§11 Order memo UNSTABLE — will change. Do not depend on it.

1. Overview

ZMAP is the Zcash Memo Asset Protocol: a way to give a shielded note an identity using only what Zcash already provides.

Zcash has no protocol-level asset support. As of orchard 0.15.3 the Ironwood pool carries no AssetBase; every attempt to emit a real issuance bundle in the reference implementation's history was silently dropped by the transaction builder.

ZMAP therefore builds asset semantics entirely out of two things a shielded note already has:

  1. A carrier note of a fixed, recognisable value.
  2. A memo that names the asset.

That is the whole mechanism. No new cryptography, no consensus change, no bridge, no oracle, no second chain. A ZMAP asset is an ordinary Zcash shielded note, so it inherits Zcash's privacy and finality unchanged.

The consequence worth stating plainly: ZMAP assets are unforgeable as notes and forgeable as identities. Anyone can write any memo. Sections 7–9 exist to close that gap, by letting an issuer prove which outputs they created and letting anyone check that proof without holding keys.

2. The carrier note

A carrier is an unspent shielded note whose value is exactly 10,000 zatoshi (0.0001 ZEC).

CARRIER_ZAT = 10_000

Exact, not a minimum. A note of 10,001 zatoshi is not a carrier.

Why this value. It is the dust floor. Below it, a wallet may sweep the note as change and destroy the asset with it. The exactness is what makes carriers mechanically recognisable and protectable: a wallet can lock every 10,000-zatoshi note carrying a valid ZMAP memo and know it has not locked ordinary funds.

Both conditions are required. A note is a carrier only if the value matches and the memo parses (§3). Checking the memo alone is a known defect: an ordinary payment whose memo happens to contain the text asset_id is not an asset. Implementations MUST enforce the value rule.

2.1 Pools

Carriers may exist in any shielded pool. Pool codes as they appear in v_received_outputs.pool in the reference implementation:

0  transparent    (never a valid carrier — no memo)
2  sapling
3  orchard
4  ironwood

An Orchard carrier still spends correctly — that pool is closed to new outputs, not to spends — but the replacement carrier produced by a transfer MUST be Ironwood on NU6.3 and later.

Transparent outputs cannot carry memos and are therefore never carriers.

3. Memo format

Zcash memos are 512 bytes. A leading 0xF6 byte means "no memo" (ZIP-302) and MUST be treated as absent.

Parsing note. Memo storage is not uniform in practice. Implementations MUST handle all three shapes seen in the wild:

  • a lead byte of 0xF6 — no memo
  • a full 512-byte block with trailing NUL padding — strip at the first NUL
  • a short, unpadded memo — length cannot be assumed

Some producers prepend 0xF5 (reserved) or omit the lead byte entirely. Take the bytes as they are, stop at the first NUL, decode as UTF-8, and trim. If the result is empty, there is no memo.

3.1 JSON dialect — the format to implement

{"asset_id":"<64 hex>","name":"…","symbol":"…","decimals":0,"metadata_uri":"https://…"}
Field Type Required Meaning
asset_id 64 lowercase hex chars yes Canonical identity (§4)
name string no Display name. Defaults to the first 8 chars of asset_id.
symbol string no Short ticker.
decimals integer no Defaults to 0.
metadata_uri string no Where the metadata JSON lives.
image_hash 64 hex chars no SHA-256 of the artwork. See §4.

A memo whose asset_id is not exactly 64 hex characters is not a ZMAP memo. Reject it; do not attempt recovery.

The JSON dialect carries identity and metadata location on chain, so no registry is required for an implementation to display and verify an asset.

3.2 Legacy prose dialect — frozen

ZecBoat Cyber-Nomad #001. Image Hash: <64 hex>. Backing: 10,000 Zats.

Grammar: <name>. Image Hash: <64 hex>. — anything after the hash is ignored.

Used by the 2026 Cyber-Nomad and ZecOil collections. Parse it for compatibility. Never emit it.

Three incompatible identifier schemes exist in the wild for these same assets and were never reconciled: one indexer derived sha256(full memo text), the published registry used sha256("ZecBoat_CyberNomads_<NNN>"), and the memo itself carries the image hash.

This specification canonicalises on the image hash for legacy memos. It is the only one of the three actually present in the memo, and it is content-addressed, so a holder can verify the artwork without trusting anyone.

Registry URIs for known legacy collections are derived per-collection, not per-asset:

registry_id  = sha256_hex("ZecBoat_CyberNomads_" + zero_padded_index)
metadata_uri = https://zecboat.com/collection/<registry_id>.json

An unrecognised legacy collection still displays from its memo and still hash-verifies its artwork. It simply shows no enrichment. Implementations MUST return "unknown" rather than guess a URI.

4. asset_id derivation

asset_id = SHA-256 of the artwork bytes, lowercase hex.

asset_id = sha256_hex(image_bytes)

This makes the identifier content-addressed: anyone holding the artwork can recompute the id and confirm the memo commits to that exact file. No registry, issuer or index has to be trusted for this check.

Relationship to image_hash. When asset_id is the content hash — the scheme this specification recommends and the reference issuer uses — it doubles as the image hash, and image_hash may be omitted. A parser encountering a JSON memo with no image_hash MUST treat asset_id as the image hash.

An issuer who wants an asset_id that is not the content hash MUST supply image_hash explicitly, or artwork verification is impossible.

Verification is exact-match only. sha256(fetched_bytes) == expected_hash, case-insensitive on the hex. A fetch failure is not verification and MUST NOT be reported as success. This is the single most common way a provenance check gets silently weakened.

5. Transfer semantics

This is the part that distinguishes a transfer from a re-issuance, and getting it wrong destroys the asset's meaning.

A transfer spends the carrier itself and writes the same memo bytes to a new carrier.

input   : the existing carrier note            (exactly 10,000 zat)
output  : a new carrier to the recipient       (exactly 10,000 zat)
memo    : byte-identical to the input's memo

Custody moves. Identity does not.

5.1 What NOT to do

Spending an ordinary ZEC note and writing a fresh memo is re-declaration, not transfer. The identity then exists in two places on chain with no input edge between them, and supply becomes whatever the issuer last claimed. Every tool in the reference implementation's history did this before it was corrected. It looks identical in a wallet UI and is fatal to provenance.

5.2 Byte-identical reproduction

The memo MUST be copied as raw bytes from the spent carrier. It MUST NOT be re-serialised from parsed fields — JSON key order, whitespace and Unicode normalisation would all be free to change, and the memo is the asset, so a different memo is a different asset.

Implementations MUST verify before building that the carrier's memo re-parses to the expected asset_id, and refuse the transfer if it does not. An asset whose identity cannot be reproduced exactly must not be transferred.

5.3 Carrier selection must be deterministic

When a wallet holds several carriers for one asset (§6), transfer MUST select among them deterministically — the reference implementation takes the minimum of (txid, output_index). This means a retry after a crash resolves to the same note rather than silently moving a different unit of the same asset.

5.4 Protecting carriers from value-driven selection

A wallet holding assets MUST prevent ordinary spends from consuming carriers. Change returns as a single ordinary note, so a carrier swept into a fee is gone.

The reference implementation locks every carrier and excludes locked notes from value-driven selection. Sending one asset requires overriding its lock, and that override MUST be scoped to the asset being sent — a wallet-wide lock owner would admit every carrier during the override, letting input selection reach for a different asset's note to make up a fee and burn it.

Derive the lock owner from the asset_id, domain-separated:

lock_owner = sha256("zecboat:zmap-carrier-lock:v1:" || asset_id)

Derived rather than random so it is stable across restarts, which makes lock-before-every-build idempotent.

6. Quantity and editions

Holdings are grouped by asset_id and counted.

Semi-fungible editions — many carriers, one identical memo — are valid and expected. Assuming one note per asset works only for 1-of-1 collections and silently under-reports everything else.

A holding is therefore {asset_id, …, quantity, carriers[]} where quantity is the number of unspent carriers. Implementations MUST NOT assume quantity == 1.

7. Proof of Issuance — the manifest

A memo asserts a name and an image hash. Nothing in it says the memo was written by the issuer it names rather than by anyone who copied the format. The manifest and the disclosure (§8) close that gap.

An issuer publishes a JSON manifest, signed with an ed25519 key held offline, binding each asset_id to the transaction that issued it.

{
  "schema": "zecboat-proof-of-issuance/1",
  "generation": 2,
  "issued_at": 1787778502,
  "issuer": { "name": "Zecboat", "collection": "Happy Birthday Zcash" },
  "network": "main",
  "assets": [
    {
      "asset_id": "a6b53dc8a1e9a0a36740bbd8720ff4c022eeb85f8f77f3a36fec5a0d47fe961a",
      "name": "Happy Birthday #001",
      "image": "/images/hbd/hbd_zcash_001.png",
      "issuance": {
        "txid": "bed008bf1024151746d5c581c0791694504f094b0cf5bf84d3818ad4f9f2ba36",
        "height": 3447455,
        "action_index": 7,
        "ock": "56121ff7d5ff924cd3aad4fa517aa5a2357dec5705a8ceac13a0988a395137e6"
      }
    }
  ]
}
Field Meaning
schema Format identifier. Reject anything not recognised.
generation Monotonic counter. See §7.1.
issued_at Unix seconds, when this generation was signed.
issuer.name Human-readable. Carries no authority of its own — the key is the identity.
network main or test. Reject a mismatch.
assets[].issuance The disclosure (§8).

Signature. Detached ed25519 over the raw bytes of the manifest file, published alongside it (issuance_manifest.sig). Verifiers MUST check the signature against the bytes they fetched, not against a re-serialisation of the parsed JSON — re-serialising changes key order and whitespace and will fail, or worse, appear to succeed against the wrong bytes.

7.1 Anti-rollback

generation MUST increase monotonically. A verifier that has seen generation N MUST reject a manifest presenting generation < N. Without this, an attacker who can serve files (a compromised web server, a hostile CDN, a network position) can replay an older, correctly-signed manifest — for example one from before an asset was withdrawn.

Persist the highest generation seen, per issuer key.

7.2 The key is the issuer

issuer.name is a label. The public key is the identity. A verifier MUST bind trust to the key, obtained out of band, and MUST NOT accept an issuer's name as evidence of anything.

Reference issuer's public key:

98508d73ce15d3f2cdc3506b3a53da0f4bece0c51116e296057de2ea922ab0db

8. The disclosure

The problem. A holder can already verify their own asset: they decrypt their note, read the memo and recompute sha256(image). Someone deciding whether to purchase holds nothing. Without disclosure they have only a signed claim pointing at an opaque shielded transaction. Provenance that only convinces existing owners is not provenance.

The primitive is the Outgoing Cipher Key (OCK), not the outgoing viewing key.

ock = Domain::derive_ock(ovk, cv_net, cmx_bytes, ephemeral_key)

The OCK mixes the sender's OVK with three values unique to one output, one-way. Therefore:

  • publishing an OCK reveals exactly one output
  • it does not reveal the OVK it was derived from
  • it does not reveal the transaction's other outputs
  • it does not reveal the recipient's other notes — that would need the IVK

A disclosure is (txid, action_index, ock) — a pointer and 32 bytes.

8.1 What a disclosure does reveal

Recovery returns the note value, the memo, and the recipient address of that output.

For an issuance output sent to the issuer's own treasury, that means the issuer discloses one of their own receiving addresses. This is a real, permanent privacy cost, borne deliberately by the issuer in exchange for public provenance. It is not incurred by the buyer, the holder, or any later transfer.

Issuers SHOULD use a dedicated address for issuance outputs they intend to disclose.

8.2 Producing a disclosure

Requires the issuer's OVK. The producer:

  1. Fetches the raw issuance transaction.
  2. Parses the Ironwood bundle.
  3. For the target action, reads the public cv_net, cmx and epk.
  4. Derives the OCK as above.
  5. Publishes (txid, action_index, ock) in the manifest.

The OVK is used in memory and MUST NOT be printed, logged or written.

9. Verification procedure

Six ordered checks. Each has a defined failure, and a verifier MUST report which check failed rather than returning a bare yes or no. A tool that collapses these into one boolean is misleading: "unverified" and "forged" are different answers.

# Check Input Pass Fail means
1 Issuer key Manifest bytes, .sig, expected public key ed25519 verifies Manifest not from this issuer, or altered
2 Freshness generation, highest previously seen generation >= seen Rollback attempt or stale copy
3 Network network field Matches the chain being queried Wrong-chain manifest
4 Asset listed asset_id Present in assets[] Issuer does not claim this asset
5 Disclosure (txid, action_index, ock) + a node Output recovers; memo's asset_id equals the claimed one Disclosure is wrong, or points at a different asset
6 Artwork Image bytes, asset_id sha256(bytes) == asset_id Artwork is not what was issued

Checks 5 and 6 need nobody's cooperation. Check 5 needs any Zcash node; check 6 needs only the image file. Checks 1–4 need the issuer's published manifest, but not the issuer's honesty at verification time — the signature was made once, offline, and cannot be retracted by controlling a web server.

Verification MUST run against a node the verifier chooses. A verifier that asks the issuer's own server whether the issuer's signature is valid has proved nothing: that server is exactly what the signature exists to distrust.

9.1 Transaction parsing notes

  • Txid byte order. Block explorers show display order, which is the reverse of internal order. Reverse before querying by hash.
  • Branch id. NU6.3 "Ironwood" is 0x37a5165b. Parse with the correct branch or the transaction will not deserialise.
  • No Ironwood bundle means nothing to disclose — report that specifically, not as a failure of the OCK.
  • Most block explorers have no Ironwood parser and will render these transactions as vin/vout 0/0 with fee 0. That is a limitation of the explorer, not a defect in the transaction.

10. Signed price list — provisional

Used where assets are offered for sale by a fulfilment service. Signed with a separate ed25519 key from the issuance key.

{
  "generation": 2,
  "issued_at": 1787772240,
  "expires_at": 1790364240,
  "prices": { "<asset_id>": 500000 },
  "merchant_address": "u1…",
  "catalogue": [ { "asset_id": "…", "name": "…", "image": "…" } ]
}

Prices are in zatoshi. merchant_address is a unified address.

Rules a fulfilment service MUST enforce, all failing closed:

  • Signature verifies over the raw bytes, against a key held offline.
  • expires_at is in the future.
  • generation has not gone backwards (persisted, as §7.1).
  • An asset with no price is not for sale. Absence is not "free" and not "any price".
  • Payment is checked against the list before delivery.

Why the key is offline. The price list is the highest-value target in the system: whoever can sign one can set every price to one zatoshi and drain the inventory legitimately. Signing happens on a machine that does not serve web traffic and does not run the fulfilment daemon. A compromised web server can then serve an old list or no list, but cannot make anything cheap.

Rate limiting. A fulfilment service SHOULD cap deliveries per unit time, persisted across restarts, so that a defect in price checking is bounded rather than unbounded. The reference implementation caps at 12 deliveries per hour.

11. Order memo — UNSTABLE

This will change. Do not build against it. It is documented so existing transactions on chain remain interpretable.

cart=[<asset_id>,<asset_id>,…]|rt:<return unified address>

The buyer sends payment to merchant_address with this memo. The fulfilment service reads the cart, prices it from the signed list, verifies the amount, and delivers to rt:.

Known limitation. The 512-byte memo caps a cart at roughly 4–6 items, depending on the length of the return address. This is a defect, not a design choice.

Planned replacement. An order id in the memo, with contents published separately. The security property that MUST survive the change: a compromised web server still cannot make anything cheap, because pricing comes from the offline-signed list rather than from the order. A new hazard the replacement MUST close: the server could alter what was ordered after payment, so the order contents have to be committed to at payment time — a hash of the contents alongside the id.

12. Security considerations

The memo is the asset. Any change to the bytes is a different asset. §5.2 is not a style preference.

12.1 Identity forgery — anyone can copy a memo

A forger copies a genuine memo byte for byte into their own 10,000-zatoshi note. The memo parses, the asset_id is unchanged, and the genuine artwork still hashes to the committed value. Every check in §9 passes except the disclosure.

Manifest membership does not help — the forgery carries the same asset_id, so it is listed too. Nor can a receiver trace the note: shielded inputs are nullifiers, unlinkable to commitments without keys, so a receiver cannot see what was spent to create their note.

Implementations MUST NOT report a note as genuine on the strength of checks 1–4 and 6 alone. Those establish that an asset exists, who issued it, and that the artwork matches — not that this note is that asset. A verifier collapsing them into one "verified" badge will endorse forgeries.

12.2 Supply inflation by splitting

Sharper than forgery, and it defeats naive ancestry checking. Any holder can spend one carrier and emit two, each carrying the same memo, funding the second from their own ZEC. Both descend from a genuine issuance. Consensus does not object — it is only balancing ZEC.

An issuer's mint count is therefore a lower bound on circulating carriers, not the supply. Transfer is safe; it creates and destroys nothing. Holding is not, and a fungible asset is meant to be held by strangers.

12.3 What closes both

Ancestry checking does not: it needs a disclosure from every previous holder, does not scale past the first hop, and is privacy-destroying where it does.

Two mechanisms, both verifiable without trusting whoever serves them.

Provenance bundle. A chain from the signed issuance output to the note in hand — per hop, the transaction id, the nullifier it spent, and an OCK for every action in that transaction. Two properties carry it: OCKs are self-authenticating, since a forged one simply fails to recover; and action counts are public, so selective disclosure is detectable. Disclose every action, count the recovered carriers, require exactly one. That is how a split is caught.

Attestation. Bundles grow per hop and each hop reveals that hop's recipient address, so the issuer may collapse the chain by verifying it once and signing:

attestation = Sign_attest( carrier_nullifier ‖ asset_id ‖ height ‖ "genuine, unredeemed" )

One signature, no chain walk, no prior holder exposed. The nullifier binds it, so it is worthless to anyone who does not control that note. An issuer MUST refuse to attest where any carrier-spend along the path produced more than one carrier.

The attestation key MUST be separate from the issuance key. Compromise of the former lets an attacker vouch for counterfeits until rotation; it must not be able to forge origin.

Implementations MUST build carrier transfers as dedicated transactions, never mixed with other spends — otherwise proving no-split later forces the seller to expose unrelated payments. This cannot be retrofitted to notes already created.

This is the banknote model: counterfeits can exist, the issuer will not honour them, and you check before accepting one. Weaker than consensus enforcement, and the best available without native assets. Cost to state rather than gloss: the issuer learns that someone is selling, and registration reveals the recipient address of that hop. Both are why it must remain optional — an unregistered transfer moves the asset perfectly well and simply arrives unproven.

Full design: the provenance design note.

12.4 Other considerations

Carriers are burnable by ordinary spends. A wallet with no carrier protection will eventually consume one as fee input. §5.4 is mandatory for any wallet that holds assets.

Issuance must be separable from custody. Provenance is meaningful only while issuance is confined to an identifiable source. A wallet that can mint is a wallet that can forge, so a wallet implementation SHOULD contain no mint path and no issuer key handling at all. In the reference implementation this is structural: there is no mint command, no mint UI and no issuer key handling anywhere in the wallet codebase; issuance lives in a separate application with separate key material.

Two signing keys, two jobs. The issuance key attests to origin and should be usable rarely and kept far offline. The price key attests to commercial terms and changes often. Do not share one key between them: compromising the shop should not let anyone forge provenance.

Fail closed everywhere. Missing price, expired list, unverifiable signature, unreachable artwork — every one of these is a refusal, never a default.

Light-client trust. A fulfilment service depending on a third-party lightwalletd depends on that server showing it every block and relaying every delivery. An omitted block means a customer paid and nothing shipped, and the operator cannot distinguish that from a quiet day. This is an acceptable risk for a casual wallet and not for a service holding paid orders. Note that shielded scanning does not disclose which addresses a client watches — trial decryption is local — so the risk here is block-withholding and relay censorship, not address disclosure.

13. What ZMAP does not prove

Stated plainly, because every one of these has been assumed at some point:

  • It does not prove who holds an asset now. Shielded notes do not reveal ownership. No engineering in this specification changes that, and none is planned to.
  • It does not prove scarcity. An issuer can issue more. The manifest records what was issued; it does not bind the issuer to stop.
  • It does not prove the artwork is original, only that it hashes to the committed value.
  • It does not make the issuer honest. It makes the issuer accountable: claims are signed, timestamped and on chain, so a change of story is detectable.
  • It is not protocol-level asset support. If Zcash gains native assets, ZMAP's carrier mechanism becomes unnecessary. The provenance layer — manifest, disclosure, verification — would still apply.

14. Reference implementation

Included alongside this document in reference/:

File Role
zmap.rs Memo parsing, carrier rules, holdings, transfer planning, carrier locking. From zecboat-core 0.6.1.
disclose_produce.rs Producer half — derives per-output OCKs from an issuer's OVK. Requires secrets.
disclose_verify.rs Verifier half — needs no secrets, only a node and a disclosure.

examples/ holds the reference issuer's live manifest and price list with their detached signatures and public keys. No private keys are included.

15. Worked example — mainnet

Everything below is on Zcash mainnet and independently checkable.

Asset — Happy Birthday #001

asset_id       a6b53dc8a1e9a0a36740bbd8720ff4c022eeb85f8f77f3a36fec5a0d47fe961a
issuance txid  bed008bf1024151746d5c581c0791694504f094b0cf5bf84d3818ad4f9f2ba36
height         3,447,455
action_index   7
ock            56121ff7d5ff924cd3aad4fa517aa5a2357dec5705a8ceac13a0988a395137e6

Verify it with the reference verifier, against any Ironwood-capable endpoint:

GRPC_URL=https://zec.rocks:443 \
TXID=bed008bf1024151746d5c581c0791694504f094b0cf5bf84d3818ad4f9f2ba36 \
INDEX=7 \
OCK=56121ff7d5ff924cd3aad4fa517aa5a2357dec5705a8ceac13a0988a395137e6 \
cargo run --release --bin disclose_verify

Expected: the output recovers, value 10,000 zatoshi, memo carrying asset_id a6b53dc8…. No keys of your own are involved.

Sales, delivered by an unattended fulfilment daemon

#001   payment   de478c2133e82fa34039d4b637f88398bc0cba9012e536a5e44353f29191be2a   3,459,438
       delivery  d3deb4d05a6bdc05899501d003aea92135f5c4e2ed8968361dc480292c3837ed   3,461,221
#002   payment   72a46e34d8032d2f55d80411e6d089a9c6a7e0fb94dd9be2082e28f6960d35ba   3,461,662
       delivery  a50d8448738c7b127eba084d02a47615d8120c8089e3f02e3c6d06026752de6d   3,461,672

A refund

056e9ba08a…   50,000 zatoshi returned of 60,000 paid (10,000 zatoshi network fee)

Public keys

issuance   98508d73ce15d3f2cdc3506b3a53da0f4bece0c51116e296057de2ea922ab0db
price      06bf6658d35b2d5a809f553eacfbd2b1f41fcf37dbfc5c46d2d9d518b6c9e3ee

16. Version history

Version Notes
0.1 Legacy prose memos. Cyber-Nomad and ZecOil collections, 2026. Three conflicting id schemes, never reconciled. Frozen — see §3.2.
0.2 JSON memo dialect introduced. Internal only. Carrier value rule enforced; re-declaration replaced by true transfer (§5).
0.3 First public release. Adds Proof of Issuance (§7), the OCK disclosure (§8) and the six-step verification procedure (§9). Documents the signed price list (§10) and the order memo (§11, unstable).

Planned for 0.4

  • Order-id memos replacing the cart memo, lifting the 4–6 item ceiling (§11).
  • Automatic inventory publishing, so a catalogue stops offering what has sold.
  • A machine-readable Proof of Issuance endpoint alongside the human-readable page.
  • A written agent verification methodology — §9 formalised for execution by AI agents, with per-step failure semantics.

Corrections and implementation reports are welcome. If you implement this and something in the document is wrong or underspecified, that is a defect in the document.