Tamper-evident receipts for AI inference. Every model answer becomes a small hashed record, records are batched into a Merkle tree, and only the root goes on chain. Each caller keeps a short proof that their answer is in the batch.
Later, anyone holding the receipt, the proof and the original text can show that a specific model version produced a specific answer at a specific time, and that nobody edited it afterwards. The operator never has to publish prompts, outputs or weights.
Once an AI system makes decisions that matter (a credit check, a claims triage, a support agent promising a refund) somebody eventually asks "what exactly did the model say, and which model was it?" Today the answer is usually a log line in a database the operator controls. That is fine until the operator is one of the parties in the dispute.
Regulators are heading the same way. Record-keeping and traceability duties for higher risk AI systems under the EU AI Act, and audit expectations in financial services, all assume logs that cannot be quietly rewritten. Anchoring a hash on a public or consortium chain is a cheap way to get that property without building a new trust institution.
inference ──► Receipt ──► leaf hash ─┐
inference ──► Receipt ──► leaf hash ─┼─► Merkle root ──► anchor (chain tx, log entry)
inference ──► Receipt ──► leaf hash ─┘ │
└─► inclusion proof per receipt
A Receipt holds:
| Field | Meaning |
|---|---|
model_id |
Stable name for the deployment |
model_digest |
Hash of the exact weights or serving image |
input_commit |
Salted commitment to the full request |
output_commit |
Salted commitment to the full response |
params |
Sampling settings, integers or strings only |
issued_at |
Unix seconds on the serving host |
Design choices that matter:
- Salted commitments. A plain SHA-256 of "yes" is trivially reversible. A 16 byte salt, kept by the operator and optionally given to the user, stops anyone from guessing short prompts out of public hashes.
- Canonical JSON with no floats. Different runtimes print floats differently, and then one receipt has two hashes. Temperature goes in as
temperature_milli: 200. - RFC 6962 tree shape. Leaf and node hashes use different prefixes, any batch size works without padding, and verification follows RFC 9162. Proofs are
ceil(log2 n)hashes, so a million receipts need 20. - Batching by size or age. One chain write per batch instead of per inference. The age limit bounds how long a receipt waits before it is anchored.
- Tiny sink interface.
publish(root, size, meta) -> ref. A Cosmos SDK message, an EVM contract call, a transparency log, or the bundledFileAnchorfor local work.
No dependencies beyond the standard library.
python -m unittest discover -s tests -v
python examples/demo.pyThe demo serves four fake answers, anchors them, then plays out a dispute: the real answer checks out, a doctored one does not.
Worth being blunt about, because this area attracts overclaiming:
- It does not prove the model computed the answer correctly. That needs zkML or trusted hardware attestation, both of which are much heavier. Receipts prove what was recorded and when, not that the recording was honest at the moment of serving.
- A dishonest operator can still refuse to issue a receipt. What they cannot do is issue one and later change it.
issued_atis the server's clock. The anchor's block time gives an upper bound that nobody controls.
docs/THREAT_MODEL.md goes through these properly, and where TEEs or zk proofs slot in when the stronger guarantee is worth the cost.
Reference implementation for design discussions and prototypes. It has not been audited.
Apache-2.0