Skip to content

Concepts ​

What is a receipt? ​

Every time your application makes an LLM call, that's a decision: a specific model was asked a specific question and gave a specific answer at a specific time. A receipt is a cryptographically signed record of that decision — who asked, what model answered, what the inputs and outputs were (or cryptographic commitments to them), when it happened, and how it turned out.

Receipts are hash-chained: each receipt binds to its predecessor (prev_hash), so any insertion, deletion, reordering, or mutation of a receipt is detectable.

The chain ​

A store is a single append-only file holding one hash chain. Every receipt in the store chains to the previous one. The chain has:

  • a chain_id (a random UUID, created when the store is opened),
  • a commitment_key (random 32 bytes, used for HMAC content commitments),
  • receipts numbered seq 1, 2, 3, … each carrying a prev_hash linking to its predecessor.

Verification walks the chain and checks, for each receipt: the sequence increments correctly, the prev_hash matches, the signature verifies under a trusted key, and the schema/suite are recognized.

Deterministic bytes (RFC 8785) ​

A receipt is signed over its canonical bytes — the RFC 8785 (JSON Canonicalization Scheme) serialization. This is deliberate, because bare JSON.stringify is not canonical:

  • -0 serializes as "0" under JSON.stringify, but RFC 8785 requires "-0".
  • Object keys use insertion order under JSON.stringify, but RFC 8785 requires UTF-16 code-unit sort.

Without canonicalization, two independent serializations of the same receipt could produce different bytes, making verification implementation-dependent and fragile. receipta canonicalizes both at emit time (so the signed bytes are canonical) and at verify time (defense in depth).

Content-optional ​

A receipt is valid whether or not it carries the actual prompt/completion content. Each receipt records content_captured — whether the bytes are present, or only cryptographic commitments (HMAC-SHA256 digests). This matters because:

  • Some integrations (OTel, the Vercel AI SDK with recordOutputs=false) don't expose content by default.
  • You may want to retain the tamper-evidence property without storing sensitive content.

When content is absent, the receipt still carries keyed HMAC commitments, so integrity is verifiable without storing the plaintext.

Trust levels ​

Every timestamp declares its trust level:

  • local_asserted — the operator's clock, no external evidence. This is the only level v0.1 populates.
  • rfc3161 — a Time Stamping Authority token (trust = the TSA's policy).
  • transparency_log — inclusion in a Merkled transparency log.
  • witness — co-signed by an independent witness.

The field exists so a receipt is never silently presented as more trustworthy than it is. v0.1 is honestly local_asserted only; the higher levels require anchoring (see the Threat Model).

Offline verification ​

receipta verify needs no network — only the receipt file and the trusted public key. The trust bootstrap is the keys/<key_id>.pub file plus the key fingerprint published on a second channel (the README / this site). Verify the fingerprint before trusting a key bundle.

Released under the Apache-2.0 License.