chain-anchor-v1 — receipt-format-agnostic public-chain anchor reference

Terminology. The canonical vocabulary on every surface is proof (grouped in folders); API responses emit the canonical names only (proof_id, proof_url, folder_slug). Where this spec shows receipt / matter / bundle_id, it is documenting a frozen on-disk/on-chain format, a stable filename (RECEIPTS.md), or a legacy route that remains accepted inbound — proof and receipt denote the same record. Full alias map: the compatibility map. In this spec, receipt most often refers to the external attestation format being wrapped (AAR, RFC 3161, C2PA) — not the Satsignal proof record; the surrounding text makes the sense clear.

This is the interop specification. For the user-facing overview, see satsignal.cloud/docs.html.

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": "<workspace-scope>",
  "issued_at": "2026-05-12T18:00:00Z"
}

Required

FieldTypeMeaning
vintSpec version. 1 for this document.
systemstringAnchor provider identifier, lowercase. satsignal for Satsignal-issued anchors; other providers MAY register their own (see §7).
chainstringPublic chain identifier, lowercase, hyphenated. bsv-mainnet, bsv-testnet, btc-mainnet, eth-mainnet.
txidstringLowercase hex transaction id on chain.
root_hashstringLowercase hex full SHA-256 of the merkle/semantic root the anchor commits to. Verifiers MUST check it against the on-chain commitment — directly where the chain envelope carries the full root, or indirectly where the envelope carries a hash of the provider's canonical document and root_hash lives inside that document (the Satsignal case). See §5 for both paths. Note the on-chain bytes are not always this value verbatim: Satsignal's envelope commits a 20-byte truncated hash of a canonical doc, and root_hash is the full root read out of that verified doc.

Optional

FieldTypeMeaning
categorystringWhat 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.
heightintBlock height. Convenience for cheap "is it buried yet?" checks.
block_hashstringLowercase hex block hash. Enables SPV-style verification without trusting an explorer.
anchor_idstringProvider-scoped opaque id, useful for fetching extended metadata via the provider's API (never required to verify).
workspacestringProvider-scoped issuer scope. For Satsignal, the workspace slug.
issued_atstringRFC 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. The slots below are non-normative integration patterns, not recommendations endorsed by the named projects. They do not imply adoption, partnership, or endorsement by AAR/BotIndex, the RFC 3161 ecosystem, C2PA, AffixIO, Visa, NVNM, or any other party — each merely shows how a public-chain anchor can be carried alongside that format without changing the format's own authority model:

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 commit multiple receipts under a single anchor, a TSA-style service committing a batch of client tokens. In all of these, the committed 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:

FieldTypeMeaning
leaf_hashstringLowercase hex SHA-256 of the canonical content of this receipt — the leaf this receipt occupies in the merkle tree.
inclusion_proofarrayOrdered 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:

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/:

Both samples include stdlib-only reproduction recipes; the existing /verify page resolves the txid, extracts the on-chain commitment (the MBNT doc_hash), re-derives the canonical document's root, and walks the proof end-to-end.

4.2 Dual-attest with RFC 3161

Some regulated workflows require or prefer RFC 3161 timestamp tokens; in those workflows chain_anchor can sit beside the TSA token rather than replace it.

The dual-attest pattern: the receipt envelope carries both

The two attestations have independent trust roots and independent failure modes:

PathTrust rootFailure modes
TSA tokenTSA's X.509 certificate chain + TSA's continued operationCert chain expires; CA revoked; TSA service shut down or unreachable; cert pinning policy diverges
chain_anchorPublic chain itself (block headers + PoW/consensus)Chain reorganization below the verifier's confirmation policy; chain/fork-policy dispute

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.

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:

  1. Fetch the transaction at txid from any node, explorer, or local copy of the chain. No provider API call required.
  2. Extract the on-chain commitment from the transaction's data output, per the provider's anchor format. Providers fall into two shapes:
    • Envelope carries the root directly. Read the committed root straight out of the data output and go to step 3.
    • Envelope carries a hash of a canonical document (the Satsignal case). The Satsignal OP_FALSE OP_RETURN layout in SPEC_mbnt.md commits a 20-byte doc_hash = sha256(canonical_doc)[:20]not a 32-byte root. Obtain the provider's canonical document (Satsignal ships it in the .mbnt bundle) and confirm sha256(canonical_doc)[:20] equals the on-chain doc_hash. The root_hash is then either the full sha256(canonical_doc) itself — the on-chain doc_hash is its 20-byte prefix (single-document anchor, per §4.2) — or a merkle/semantic root carried as a field inside that now-verified document (manifest / batched anchor, per §4.1).
  3. Check the chain_anchor's root_hash matches the root established in step 2 — the value read directly from the envelope, or the root read from the verified canonical document. Mismatch → reject.
  4. Check confirmations meet the verifier's policy. Below policy → treat as pending, not verified.
  5. Verify receipt content against root_hash using 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. Adopters SHOULD NOT market chain_anchor as "proof of existence." The full, canonical statement of the guarantee — tamper-evidence and an upper time bound, not authorship and not prior existence — is in the bundle spec; this paragraph is the chain-anchor-v1-scoped restatement of it.

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 by the issuer — their canon, not Satsignal's), 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

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

Available now. No library is required to verify — §5 is a complete stdlib-only recipe (sha256 + a single chain fetch). The in-browser verifier at /verify also accepts a pasted receipt carrying chain_anchor and reports each of the 5 verification steps independently.

Planned (not yet published). Packaged stdlib-only helpers — chain_anchor.py (Python) and chain-anchor.js (browser + Node) for encode / decode / verify against a fetched BSV transaction — are forthcoming. They are conveniences only: this document is the wire-level contract and §5 already lets any implementer verify without them. This section will link them once published; until then there is deliberately no file at those names to fetch.

Questions about this specification? Email hello@satsignal.cloud.