chain-anchor-v1 — receipt-format-agnostic public-chain anchor reference
Status: draft 1, 2026-05-12. Audience: anyone shipping an AI-agent receipt format (AAR, RFC 3161-wrapped, C2PA, consent-receipt, custom JSON) who wants a second, issuer-independent proof root. Goal: define a tiny canonical object any receipt format can embed to point at an immutable public-chain commitment of the same payload, so a verifier can fall back to the chain if the issuer's signing key, TSA cert, or hosted ledger ever becomes untrusted or unavailable.
The field is intentionally generic. Satsignal is the reference implementation; any anchor provider that writes a root hash into a public chain can fit the same shape.
1. Why this exists
Every receipt format in the agent-receipt space (Mar 2026 survey) bottoms out in one trust root: an issuer Ed25519 key, a TSA X.509 chain, a private L2's validator set, or a vendor's hosted verifier. Each is a single point of revocation, compromise, or shutdown. A chain_anchor field lets the same receipt carry a second, independent proof — anchored in a public chain that the issuer does not control — without forcing the receipt format to change anything else.
One field. No SDK dependency. Verifiable offline with a block explorer and stdlib hashing.
2. The object
Canonical JSON. Required fields first; verifiers MUST reject if any required field is missing or malformed.
{
"v": 1,
"system": "satsignal",
"chain": "bsv-mainnet",
"txid": "<64-hex>",
"root_hash": "<64-hex, sha-256>",
"category": "commitment",
"height": 891245,
"block_hash": "<64-hex>",
"anchor_id": "anc_01HXYZ...",
"workspace": "o_RuM4CKvx0q",
"issued_at": "2026-05-12T18:00:00Z"
}
Required
| Field | Type | Meaning |
|---|---|---|
v | int | Spec version. 1 for this document. |
system | string | Anchor provider identifier, lowercase. satsignal for Satsignal-issued anchors; other providers MAY register their own (see §7). |
chain | string | Public chain identifier, lowercase, hyphenated. bsv-mainnet, bsv-testnet, btc-mainnet, eth-mainnet. |
txid | string | Lowercase hex transaction id on chain. |
root_hash | string | Lowercase hex SHA-256 of the merkle root committed in the transaction's anchor envelope. Verifiers MUST check this against the on-chain payload. |
Optional
| Field | Type | Meaning |
|---|---|---|
category | string | What is being anchored, in the provider's taxonomy. For Satsignal: one of commitment, evidence_bundle, policy_snapshot, reveal. Other providers define their own values. Verifiers that don't recognize the value MUST treat the anchored payload as opaque and rely on the outer receipt for interpretation. |
height | int | Block height. Convenience for cheap "is it buried yet?" checks. |
block_hash | string | Lowercase hex block hash. Enables SPV-style verification without trusting an explorer. |
anchor_id | string | Provider-scoped opaque id, useful for fetching extended metadata via the provider's API (never required to verify). |
workspace | string | Provider-scoped issuer scope. For Satsignal, the workspace slug. |
issued_at | string | RFC 3339 UTC timestamp the anchor was issued. Informational; the chain's block time is authoritative. |
Unknown fields MUST be ignored, not rejected, so providers can extend the object without breaking existing verifiers.
3. String form (compact)
For HTTP headers, query strings, log lines, or any place a single token is more practical than a JSON object:
chain-anchor:1:satsignal:bsv-mainnet:<txid>:<root_hash>
Colon-separated, lowercase, no whitespace. Field order is fixed: chain-anchor, v, system, chain, txid, root_hash. Optional fields are not representable in the string form — use the JSON object when you need them. A verifier MUST accept either form and produce the same result.
4. Where to embed it
The spec defines the shape, not the placement. Recommended slots:
- AAR (BotIndex): top-level optional
chain_anchorfield on the receipt, included in JCS canonicalization so the issuer's Ed25519 signature also covers the anchor reference. - RFC 3161 / TSA-timestamped receipts: parallel proof on the same receipt envelope. Receipt is dual-attested: TSA token +
chain_anchor. Compliance regimes naming RFC 3161 are satisfied; long-tail verification falls back to the chain. See §4.2 for the embedding shape and verification recipe. - AffixIO consent receipts:
proof.chain_anchoron the consent object; the anchor points to the agent run that consumed the consent. - C2PA: custom assertion label
com.chain-anchorcontaining the JSON object verbatim. The trailing.vNis intentionally omitted from the label — c2pa-rs treats.vNsuffixes as a separate version field and normalizes them away during storage, so an in-label version reads back stripped. The envelope's ownvfield (§2) carries the spec version; readers MUST trust that rather than the label suffix. A worked example with a real ES256-signed manifest carrying this assertion (cert intentionally not in the C2PA production trust list, so the cert path fails while the chain path verifies) is at/samples/chain-anchor/c2pa-credentialed.jpgwith the matching envelope at/samples/chain-anchor/c2pa-credentialed.json. Co-exists with X.509-signed manifests. - Visa Trusted Agent Protocol (TAP): TAP's wire format is RFC 9421 HTTP Message Signatures over agent-merchant requests, with agent identity verified against a registry-published public key. A chain anchor can carry the agent's registration record and any number of specific signed requests as batched leaves under one on-chain root, giving merchants and auditors a permanent record of (a) what key the agent was registered under at time T and (b) what specific requests the agent signed at time T — neither of which the TAP registry's then-state needs to remain online to verify. A worked example with a real Ed25519-signed RFC 9421 request and a TAP-style registration record batched under one BSV anchor is at
/samples/chain-anchor/tap-interop.jsonwith the reproduction script at/samples/chain-anchor/tap-demo.py. Built from the public RFC 9421 standard; no code redistributed from Visa's reference repository. - NVNM Chain checkpoints: each NVNM block batch publishes a single
chain_anchorto BSV; the L2 block header carries the reference. - Generic JSON: top-level
chain_anchorkey.
When a receipt carries more than one anchor (e.g. a batched evidence bundle anchored both in Satsignal and a partner chain), use chain_anchors: [ ... ] instead. Verifiers MUST accept both shapes.
4.1 Batched anchoring
Many useful anchoring patterns commit multiple receipts under a single chain transaction — e.g. an L2 checkpointing its hourly block roots into one BSV transaction, an AAR issuer batching receipts to amortize anchor cost, a TSA-style service committing a batch of client tokens. In all of these, the on-chain root_hash is a merkle root over N leaves, and each leaf is the content hash of one participant.
A chain_anchor carried by an individual receipt in such a batch SHOULD include two additional optional fields:
| Field | Type | Meaning | |
|---|---|---|---|
leaf_hash | string | Lowercase hex SHA-256 of the canonical content of this receipt — the leaf this receipt occupies in the merkle tree. | |
inclusion_proof | array | Ordered path from the leaf to the on-chain root, per SPEC_merkle_row.md: each entry is `{"sib": "<64-hex>", "side": "L" | "R"}`. Leaf-level entries first, root-level entries last. |
Verification adds two steps to §5 between the existing steps 3 and 5:
- 3a. Recompute the receipt's content hash and check it equals
leaf_hash. Mismatch → reject. - 3b. Walk
inclusion_prooffromleaf_hash. For each entry, ifside == "L"setcarry = sha256(sib || carry), elsecarry = sha256(carry || sib). The finalcarryMUST equalroot_hash. Mismatch → reject.
When leaf_hash and inclusion_proof are both absent, the receipt is the entire anchored payload (the single-receipt case in §5). When both are present, the receipt is one leaf in a batch.
Two worked examples are hosted at /samples/chain-anchor/:
batch-anchor.json— four AAR-shaped leaves batched under one real BSV anchor (txid1ba57f41…15783e, root1c724e6fb05e7355…b861d0989). Homogeneous receipts, single issuer — the "amortize one anchor across N receipts" pattern.cross-domain.json— three leaves drawn from three independent public systems (Ethereum mainnet block hash, HuggingFace model card README at a pinned commit, C2PA-credentialed test image) batched under one BSV anchor (txid027c5b69…f075, root686c620b9b0e7219…f3ebfb7). Heterogeneous evidence — the shape this spec promises but the AAR-only sample doesn't yet demonstrate.
Both samples include stdlib-only reproduction recipes; the existing /verify page resolves the txid, extracts the on-chain root, and walks the proof end-to-end.
4.2 Dual-attest with RFC 3161
Many regulated workflows require — or strongly favor — an RFC 3161 timestamp from a Trusted Timestamping Authority. eIDAS Article 41(2) names this directly in the EU; several U.S. state evidence statutes and federal compliance regimes treat it as a recognized form of trusted timestamping. A chain_anchor does NOT replace a TSA token in those workflows — it sits alongside it.
The dual-attest pattern: the receipt envelope carries both
- a TSA-issued RFC 3161 token over
sha256(canonical_receipt), and - a
chain_anchorwhoseroot_hasheither equalssha256(canonical_receipt)(single-receipt anchor) or includes it as a leaf in a batched anchor per §4.1.
The two attestations have independent trust roots and independent failure modes:
| Path | Trust root | Failure modes |
|---|---|---|
| TSA token | TSA's X.509 certificate chain + TSA's continued operation | Cert chain expires; CA revoked; TSA service shut down or unreachable; cert pinning policy diverges |
| chain_anchor | Public chain itself (block headers + PoW/consensus) | Reorg below confirmation policy; chain itself disputed (vanishingly unlikely for established chains) |
A verifier MAY accept either attestation as sufficient. Receipts are typically verified through both paths in parallel; long-tail verifiability (years after issue, when TSA infrastructure has rotated keys multiple times) leans on the chain path. Short-tail compliance check ("did a trusted timestamping authority sign this when it was issued?") leans on the TSA path.
Embedding shape. For receipt formats with a typed evidence array (such as AAR's evidenceRef[]), each attestation is its own entry — rfc3161/v1 for the TSA token and chain-anchor/v1 for the chain anchor. The rfc3161/v1 entry carries tsaToken (base64-encoded TimeStampResponse, ASN.1 DER per RFC 3161 §3.3), tsa (TSA identifier), and messageImprintHex (the canonical hash the token was issued over). For receipt formats without a typed array, the spec recommends sibling fields tsa_token + chain_anchor on the receipt envelope.
Order-of-operations subtlety. The TSA token is issued over the canonical receipt at a specific point in time; canonicalization for the TSA's message imprint MUST therefore EXCLUDE the eventual rfc3161/v1 entry (and any signature byte field — for AAR, that's signature.sig per §5.1 of the AAR spec). A verifier reconstructs the canonical bytes by stripping those fields, hashing, and comparing against the token's messageImprint.
Verifier rules.
- If only one of {TSA,
chain_anchor} verifies, the verifier SHOULD report success with a note indicating which path failed and why. Single-path success is still strong evidence. - If both fail, the verifier reports failure (the receipt's own signature still gives issuer attribution but no third-party timestamp).
- If both succeed and their attested timestamps disagree by more than a verifier-policy threshold (e.g. 1 hour), treat as suspicious — divergence is expected (TSA timestamps are seconds-precision; chain timestamps are block-time, often minutes delayed) but large gaps suggest stale or tampered data.
A worked example — the same AAR receipt as §4.1, now with both a real RFC 3161 token from freetsa.org and the real BSV anchor — is hosted at /samples/chain-anchor/aar-receipt-2-dualattest.json. RECEIPTS.md includes an openssl ts -verify recipe for the TSA path. Both paths verify independently.
5. How to verify (stdlib-only)
Given a receipt and its embedded chain_anchor:
- Fetch the transaction at
txidfrom any node, explorer, or local copy of the chain. No provider API call required. - Extract the anchor envelope from the transaction's data output (per the provider's anchor format — for Satsignal, the documented OP_RETURN layout in
SPEC_mbnt.mdcontaining the version, subtype, and root hash). - Check
root_hashmatches the value extracted from the envelope. Mismatch → reject. - Check confirmations meet the verifier's policy. Below policy → treat as pending, not verified.
- Verify receipt content against
root_hashusing whatever inclusion proof the receipt format carries (merkle path, single-leaf hash, full-payload re-hash). Satisfied → the receipt is anchored.
No SDK, no account, no network call to the issuer. The verifier needs: a way to read a transaction, SHA-256, and the receipt's own inclusion proof scheme.
What anchoring proves
A successful verification establishes that the issuer of the receipt knew root_hash at or before the block time of txid. It does not prove the underlying content existed before that time (see the Satsignal trust page for the general framing). Adopters SHOULD NOT market chain_anchor as "proof of existence."
6. Worked example — AAR receipt with chain_anchor
{
"v": "aar-1.0",
"issuer": "did:web:botindex.io",
"agent": "did:agent:abc",
"action": { "type": "purchase", "amount_cents": 4200, "merchant": "acme" },
"timestamp": "2026-05-12T17:59:58Z",
"nonce": "0f3c...",
"chain_anchor": {
"v": 1,
"system": "satsignal",
"chain": "bsv-mainnet",
"txid": "a3f1...e29c",
"root_hash": "7b91...44d2",
"category": "commitment",
"height": 891245,
"anchor_id": "anc_01HXYZABCDEF"
},
"signature": "ed25519:..."
}
The Ed25519 signature covers the entire object (JCS-canonicalized), including chain_anchor. A verifier with a stale or revoked issuer key can still establish that the receipt's root_hash was committed on BSV at height 891245.
7. Versioning and provider registry
v: 1is this document.- Additive optional fields do not bump the version.
- Renaming, removing, or changing semantics of required fields requires
v: 2and a new field name (chain_anchor_v2) to avoid ambiguity in co-existing receipts. - New
systemorchainvalues do not require a version bump; they are values, not schema.
Provider registry. The only currently recognized system value is satsignal. Additional providers are registered by pull request against this document. A registry entry is one line: the lowercase identifier and a link to the provider's anchor-envelope spec (equivalent of Satsignal's SPEC_mbnt.md), so that step 2 of §5 is unambiguous for verifiers.
8. Reference implementation
- Python:
chain_anchor.py(stdlib only) — encode, decode (JSON- string form), verify against a fetched BSV transaction.
- JavaScript:
chain-anchor.js(stdlib only, browser + Node) — byte-identical canonicalization with the Python helper. - Live demo: the in-browser verifier at
/verifyaccepts a pasted receipt withchain_anchorand reports each of the 5 verification steps independently.
(Reference implementations to land in subsequent commits; this document is the wire-level contract they target.)
Source: docs/notary_spec/chain-anchor-v1.md.
Email hello@satsignal.cloud
for clarifications.