Your AI agent just refunded a customer, deleted a database table, or emailed a spreadsheet to the wrong person. Three weeks later, someone asks what happened.
What do you have?
A log line.
Who wrote it?
Your own code.
Could it have been edited?
Absolutely.
That is the state of most AI audit trails today, and they're only as trustworthy as whoever controls the logs.
Payment systems solved this decades ago. You don't get an assertion that money moved—you get a receipt that anyone can verify.
This post shows how to give every LLM response a cryptographically verifiable receipt, what that receipt actually proves, and just as importantly, what it doesn't.
Everything below ships in an MIT-licensed gateway you can run with a single command. You don't even need an API key:
npx @axiorank/gateway demo
What an AI response receipt needs to contain
A useful receipt should commit to at least four things:
- The exact request bytes received by the gateway (stored as a SHA-256 hash)
- The exact response bytes returned (also stored as a SHA-256 hash)
- Everything the gateway did in between:
- routing decisions
- selected provider
- guardrail verdicts
- redactions applied
- token usage
- Its position in history so previous records cannot be silently modified.
A real receipt (trimmed) looks like this:
{
"kind": "axiorank-gateway-receipt-v1",
"request": {
"modelRequested": "gpt-4o-mini",
"bodySha256": "..."
},
"route": {
"alias": "axio/auto",
"strategy": "cost",
"served": {
"upstream": "groq",
"model": "llama-3.3-70b-versatile"
}
},
"guardrails": {
"prompt": {
"decision": "allow",
"redactions": 0
},
"completion": {
"decision": "allow",
"redactions": 1
}
},
"response": {
"status": 200,
"bodySha256": "...",
"inputTokens": 45,
"outputTokens": 118
},
"chain": {
"seq": 42,
"prevReceiptHash": "..."
},
"keyId": "a1b2c3...",
"algorithm": "EdDSA",
"signature": "..."
}
Why canonicalization matters
Signing JSON is harder than it sounds.
JSON doesn't have a single canonical byte representation. Different serializers can reorder keys, change whitespace, or format numbers differently. Even though the data is identical, the bytes change, which invalidates signatures.
The solution is RFC 8785 (JSON Canonicalization Scheme), which defines one deterministic serialization that every verifier can reproduce.
The receipt payload is:
- Canonicalized using RFC 8785
- Signed using Ed25519
- Stored with a detached signature
Verification only requires standard cryptography libraries—you never have to trust the gateway's own implementation.
Turning receipts into an audit trail
A signed receipt proves one response.
An audit trail has to prove something more important:
- nothing was deleted
- nothing was reordered
- nothing was modified
Every receipt includes:
- a sequence number
- the hash of the previous receipt
Together they form a hash chain stored as JSONL.
If any historical byte changes, every receipt after it fails verification.
npx @axiorank/gateway verify ~/.axiorank/gateway/receipts.jsonl
# receipt chain valid (128 receipts, key a1b2c3d4)
The signing key is generated locally on first run and published through a local JWKS endpoint.
No account.
No cloud service.
No central authority.
What receipts do not prove
It's important to be precise.
A receipt does not prove anything about what happened inside the model itself.
It only proves what crossed the gateway boundary.
It also doesn't prove the gateway operator is honest.
If the operator is your adversary, you need an external transparency log or witness service (that's what the hosted version provides).
The local receipt chain provides tamper evidence.
If someone edits history, the chain simply stops verifying.
Blocked requests also generate receipts, turning "our filters caught it" into something that can actually be verified later.
Guardrails belong at the gateway
Since every model request already passes through the gateway, it's the natural place for baseline security.
The gateway can:
- block prompt injection
- block destructive commands
- redact API keys
- redact PII
- score requests and responses
- include every decision in the signed receipt
Everything runs in-process.
No second network request.
Enabled by default.
The guardrails are heuristic-based with scored confidence bands—not a silver bullet—and benchmark results against open-source alternatives at a fixed false-positive rate are published.
Try it
# OpenAI-compatible gateway on :8787
npx @axiorank/gateway
# Full offline demo (~15 seconds)
npx @axiorank/gateway demo
The project is MIT licensed with zero runtime dependencies and includes:
- 34 provider presets
- LangChain examples
- LiteLLM examples
- Vercel AI SDK examples
- Ollama examples
- CrewAI examples
- CI integration examples
GitHub:
Top comments (1)
The chain is tamper-evident against everyone except the one party your opening scenario is about. The gateway mints its own key on first run and serves its own JWKS, so the operator sits inside the key's trust domain: rewrite receipt 40, re-sign 41 through 128, and verify still prints "receipt chain valid" with a straight face. What it genuinely stops is a tamperer who does not hold the key, like a file-level edit to the JSONL from a compromised process. That is worth having. But the person asking what happened three weeks later is a third party, and to them a locally-keyed chain is still a claim the gateway wrote about itself, now carrying a signature the gateway also issued. Forgery got more expensive. The category didn't move.
There is also nothing binding a receipt to time, or to one history. An operator can keep two chains that both verify, show one to auditor A and the other to auditor B, and neither auditor can see the fork from the copy they hold. seq orders events with respect to a key, not a clock.
The repair doesn't have to be a hosted witness service. Make the receipt a two-party artifact: have the client supply a nonce in the request, countersign the response hash with its own key, and keep its own copy of the chain head. History then can't be re-authored without the counterparty's key, and two divergent chains become a provable conflict instead of two valid files. That is non-repudiation toward the party that actually cares, with no central transparency log sitting in the trust path.
Smaller and separate: bodySha256 commits to the response your gateway assembled, not the one the client received. Under SSE with a mid-stream disconnect you get a receipt that verifies cleanly for bytes that never landed.