Draft 0.1. A portable receipt chain for agent-initiated payments that detects modification relative to a trusted receipt hash or external anchor. Published so anyone can implement and verify it without Cartulary.
A cartulary is a bound collection of charters and deeds. This standard treats the record of a payment the same way: an append-only chain of receipts, each one hashing its predecessor, verifiable by recomputation, and disclosable per leg on a need-to-know basis.
Status: draft, extracted from the running implementation at cartulary.xyz. The console's Evidence screens and its exported bundles conform to this document, and the example bundle in this repository verifies with the verifier in this repository. Expect breaking changes until 1.0.
sdk/ holds cartulary, the TypeScript SDK that produces these
receipt chains from the paying side: one call decides a payment against the
agent's mandate and its organisation's signed policy, signs the issued
template with a key Cartulary never sees, and waits for the settled receipt.
Two dependencies, no RPC. Its README documents the result and error model;
examples/pay.mjs is a runnable example.
node verify.mjs examples/evidence-bundle.json
No dependencies. The verifier recomputes every hash from each receipt's own fields and checks linkage and sequence. Three adversarial vectors ship beside the example and run in CI on every change:
tampered-bundle.json: one payload field altered; verification fails.truncated-bundle.json: the tail dropped; verification passes, which is the documented boundary, not a bug (see below).partially-disclosed-bundle.json: internal payloads withheld; disclosed receipts verify by recomputation, withheld ones by linkage alone.
Every event in a payment's life is one receipt:
| Field | Type | Meaning |
|---|---|---|
payment_id |
string | The payment's identifier: the bundle's payment.id. Present on every receipt so each is verifiable alone |
env |
string | live, simulated, or testnet. Present on every receipt |
seq |
integer | Position in the chain, starting at 1, no gaps |
event_type |
string | initiated, evaluated, screened, allowed, held, investigating, released, blocked, escalated, reported, settled, or refused |
actor |
string | Who wrote it: agent:<name>, operator:<session>, or a system component |
payload |
object or null | Event detail; null when withheld under disclosure |
prev_hash |
string or null | The previous receipt's hash; null for seq 1 |
hash |
string | This receipt's hash; see construction below |
created_at |
timestamp | When the event happened; not part of the hash |
Receipts are append only. A correction is a new receipt, never an edit.
hash is the lowercase hex SHA-256 of the RFC 8785 (JCS) canonical JSON of
exactly these seven fields:
{
"actor": "...",
"env": "...",
"event_type": "...",
"payload": { },
"payment_id": "...",
"prev_hash": "... or null",
"seq": 1
}Two payload rules make canonicalisation exact by construction:
- Amounts are decimal strings, never JSON numbers. The scale is set by the asset (this implementation uses six decimal places for its instruments); the rule is the string, not the scale.
- Numbers appearing in payloads are integers only (sequence numbers, versions). Anything fractional travels as a string.
Recomputing every hash and checking linkage and sequence proves that no disclosed receipt was altered, reordered, or removed from the interior of the chain, relative to the hashes in hand. Three limitations are part of the design and are stated rather than hidden:
- Self-consistency is not authorship. A hash chain has no signature. A party holding the whole chain could rewrite an event and recompute every subsequent hash, and the result would still verify. Detection of that requires a trusted chain head, a signature over the head, or an external anchor; anchoring is on the Cartulary roadmap and will be added to this standard when it ships.
- Truncation and omission are invisible to recomputation. A chain cut at the tail, or a whole payment's chain withheld, verifies or simply does not appear. The same anchoring closes this.
- Partial disclosure verifies less, and says so. A counterparty receives its own receipts and every hash. Disclosed receipts verify by recomputation; withheld receipts are opaque commitments verified by linkage alone. Their contents are not proven to the recipient, and the verifier reports the difference.
A standard that overstated its own proofs would fail its own test.
A receipt records what a control plane decided. Two boundaries follow from that, and they belong here rather than only in a product's marketing:
- The decision and the settlement are not one state transition. An approval is validated against the prepared transaction before signing and reconciled against the ledger afterwards. Settlement can succeed while the receipt write fails, leaving the account of it briefly incomplete and repaired by observing the ledger. That is recovery, not a guarantee.
- A signature commits to the transaction, not to the authority behind it. Unless a commitment to the mandate, the policy version, and the decision is carried inside the transaction, the binding between them is an assertion held by whoever kept the records. Carrying that commitment is the next item in this standard.
The implementation's own version of this argument, including why a central multi-tenant service is the wrong long-term shape for it, is at cartulary.xyz/architecture.
A payment that violates its mandate or policy is refused before construction:
no value moves, and the attempt is receipted (initiated then refused,
with the rule that refused it in the payload). A control plane that cannot
show its refusals has never been tested by one.
examples/evidence-bundle.json is a real bundle exported from the running
system: the payment summary, its full chain with every hashed field on every
receipt, and a verification block recomputed at export time. Schema name:
cartulary/evidence-bundle/0.1.
canton/ holds a narrow external-signing validator, run against
a local Canton network using Canton's example Iou model: a transaction
prepared by a participant, decoded and checked against the approved intent,
its hash recomputed independently, refused when any of it disagrees, and
signed only when it all does. The recorded run includes three tampered
transactions whose hashes were recomputed to stay internally consistent, so
that they can only be caught by comparison with the approval.
examples/canton-adapter.md sets out the
reasoning: the two gates, and what each half of the system can and cannot
prove alone.
It is not a stablecoin settlement, it is not integrated into the hosted application at cartulary.xyz, and it has not been run on DevNet, TestNet, or MainNet, which needs a participant to sponsor access.
The EVM implementation of the same gate-two obligation is real and tested:
sdk/src/transaction.ts decodes the prepared
transaction, checks it against the approved intent, and recomputes its hash
before signing.
Machine-readable JSON Schemas for receipts, bundles, and per-event payloads; organisation-level anchoring; a commitment to the mandate, policy version, and decision carried inside the transaction, so that the binding between authority and execution is checkable by a counterparty rather than asserted by a service; and the Canton adapter above, run against a shared network rather than a local one.
Apache-2.0; see LICENSE. Implement it, fork it, or tell us what is wrong with it: issues are welcome, and corrections are made in public.