Receipt Spec v1
This page specifies the Dynamic Feed receipt format exactly as the production system issues and verifies it. It is written for an adverse party: someone who does not trust Dynamic Feed and wants to check a record anyway. A receipt is a self-contained JSON record of what named sources reported at a stated moment, signed at issue, optionally timestamped by independent authorities, and archived append-only.
A valid receipt proves integrity and origin. It never proves the underlying claim is true in the world. Advisory evidence, not certification. Tamper-evident, not tamper-proof.
1Canonical form
Every hash and signature in this family is computed over canonical bytes:
JSON, object keys sorted lexicographically (recursively; arrays keep their order)
compact separators: "," and ":" with no whitespace
ensure_ascii escaping: every code point above U+007E becomes lowercase \uXXXX
(astral code points become UTF-16 surrogate pairs)
numbers verbatim as issued (ints bare, floats shortest round-trip form)
encoded as UTF-8
reference: json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
hashing: SHA-256 over the canonical bytes, lowercase hex
name: "json-sorted-compact" (stated inside every signature block)
Verifiers must parse the served JSON losslessly, keeping each number token exactly as received. Re-serializing numbers through a lossy float parse can change bytes and falsely fail a signature. The in-browser verifier at /verify and the df-verify packages do this for you.
2The envelope signature
"signature": {
"alg": "Ed25519",
"key_id": "df-ed25519-4cb32e72f333",
"canonicalization": "json-sorted-compact",
"sig": "<base64url of the 64-byte raw Ed25519 signature>"
}
- key_id is
df-ed25519-plus the first 12 hex characters of the SHA-256 of the raw 32-byte public key. Check that the key you fetched hashes to the key_id you fetched it under. - sig is padded base64url as served; accept padded and unpadded forms.
- Signing happens at issue time, before any anchor exists.
3The strip rule
To reconstruct the signed body, remove exactly two top-level fields: the signature block and the anchor block. Then canonicalize. The anchor must be stripped because anchoring happens after signing: the anchor block is attached once the signature already exists, so it can never be inside the signed bytes. Stripping anchor from a document that has none is a no-op, so one rule covers every schema in the family. Nothing below the top level is ever stripped.
4Receipt ids
id = "<prefix>_" + first 16 hex chars of
sha256( canonical( document minus the id field, signature and anchor ) )
live prefix: "ans_" (field receipt_id on answer/v1)
issue order: build payload, compute id, sign, anchor
The id is content-addressed, so any post-issuance edit both breaks the signature and falsifies the id: two independent recomputations you can run offline.
5Anchor states and what each proves
digest_sha256, the SHA-256 over the canonical form of the signed document without its anchor field (signature included), plus instructions for obtaining an RFC 3161 token for that digest from any public TSA. Proves nothing about time by itself; it pins the exact digest and makes independent anchoring a one-command act for the holder. Dynamic Feed is not a dependency for proof-of-when.Anchors prove WHEN, never THAT the content is true. Anchoring is never load-bearing: a receipt whose anchoring failed is still a valid signed receipt.
6Key discovery
GET /.well-known/keys returns a JSON map of key_id to base64url raw 32-byte Ed25519 public key: the current signing key AND every former key. Old keys are published forever; rotation adds and never removes, so a receipt signed years ago keeps verifying. The same key set is pinned outside the domain in the public df-verify repository and the verifier packages on PyPI, npm and crates.io. With a pinned copy, verification needs zero network calls.
7Verification procedure
You need the receipt exactly as served and a copy of the key set. No account, no API key, no other call to Dynamic Feed. Everything runs offline.
- Parse losslessly. Keep every number token verbatim.
- Apply the strip rule. Remove the top-level
anchor, then the top-levelsignature. Keep both blocks for steps 5 and 6. - Canonicalize the body and compute its SHA-256.
- Verify the signature. Ed25519 over the canonical body bytes against the public key for
signature.key_id. Check the key hashes to the key_id. - Recompute the id (receipt-class documents): remove the id field as well, canonicalize, SHA-256, first 16 hex chars, prepend the prefix, compare.
- Validate the anchor, if present. Canonicalize the document minus
anchoronly (signature included), SHA-256, compare toanchor.digest_sha256, then validate the RFC 3161 token or DLT-timestamp proof against that digest with standard tooling. - Cross-check the checkpoint. The receipt's issue day should appear under archive-day/v1 at GET /v1/checkpoints, with the day's digest chained to the previous day and independently timestamped.
- Read the result honestly. A pass proves the bytes are exactly what the key holder signed at a bounded time. Sources can still be wrong. The receipt's own disclaimer travels inside the signed body so this boundary cannot be stripped from a verifying record.
8The checkpoint cross-check
Every issued receipt is appended to the permanence archive. Once a day, the day's archive partition is enumerated and committed as an archive-day/v1 checkpoint: a SHA-256 digest over the sorted object listing (the recipe travels inside the document), per-kind counts, and a prev_digest chaining it to the previous day. The checkpoint is signed with the standard envelope and its digest is anchored. Silently rewriting any archived object afterward changes the day's digest, which breaks both the chain and the anchored timestamp. See /checkpoints.
9Timeless rules
- No field alters after issuance. Only detached anchor proof bytes may be upgraded in place (pending to confirmed); the signed document never changes.
- Corrections are new receipts referencing the old receipt's id. The old receipt stays served and stays verifiable. History is never rewritten.
- Divergence is never silently resolved. When independent sources disagree, the record says diverge (concordance/v1), shows every reading, and never blends them. single_source is the honest fallback.
- Old keys are published forever.
- Signed proves integrity, not truth. Advisory evidence, not certification. Never in the money path, never the decision.
10Scope today
Conformant now: answer/v1 (the full spec including receipt ids and anchor states), trigger-receipt/v1 (canonical form, signature, strip rule; anchoring holder-driven via POST /v1/anchor), and archive-day/v1 (canonical form and signature; its anchor proof lives beside the document). Schemas that predate this spec (awareness/v1, attestation/v1, intensity/v1, nearby/v1, fact/v1, the DF-VERIFY/1 receipt/v1 family) already share the canonical form, the signature block and the strip rule, and adopt the content-addressed id and uniform anchor block at their next version. This spec renames and removes nothing: live consumers keep parsing every existing field.
Golden vectors and the conformance test live in the repository at tests/spec/. The canonical markdown of this spec is specs/receipt-spec-v1.md; this page renders it. Machine-readable schemas: trigger-receipt-v1.json, attestation-v1.json, receipt-v1.json, awareness-v1.json.