Receipts — what gets published, what never does
A receipt is a signed, published record of what ModelRig measured on one bake-off, run, or route. It is the one document shape (Receipt@v1) that six growth loops ride: a customer (or ModelRig, for launch reports) can publish any receipt as a login-free page anyone can verify — without the public path ever touching a database, and without any content ever leaving the org.
Hook: See. Grade. Improve. A receipt is the evidence behind "Improve" — the measured proof a swap carries, not an assertion.
What a receipt contains
Receipt@v1 (modelrig/receipts):
| Field | What |
|---|---|
kind | bakeoff · regret · run · fact-sheet (+ masking-proof, egress, disclosure, statement, launch-report as their missions land) |
issuer.org_alias | the org's chosen public alias — never org_id |
subject.*_alias | route/task/pipeline names, aliased by default |
measured | numbers, booleans, enums, hashes, identifiers — scores, counts, CIs, n, costs |
models | provider/model (+ snapshot) compared |
honest_limits | verbatim claim-ceiling lines from the producing spec |
hash | sha256:<hex> over the canonical JSON of everything above |
signature | Ed25519 (alg, key_id, sig) over the hash |
What NEVER gets published
The classification gate (isReceiptSafe, property-tested) runs before anything is signed or written and refuses (fail-closed) a receipt where:
- any
measuredvalue is not a number / boolean / null / metadata-safe string (≤128 chars, no whitespace) — **no free text, no prompt/output text, no raw confidence scores, no dictionary values**; - any
measuredkey names a tenant id (org_id,project_id,tenant_id,user_id, …) — aliases only; - any alias marker is anything but the literal
"aliased".
So: content never leaves. What would be content is a hash or a count.
Aliasing
Route, task, and pipeline names are aliased by default — the published object shows route-a1b2c3, not your internal route name. The alias is a stable, non-reversible label (per-org); the raw→alias mapping never leaves the org (only the fact that a field was aliased, as aliases.<field> = "aliased"). An org can opt to reveal a field per publish (reveal: { route: true }).
Publishing, listing, revoking
- Publish —
POST /v1/receipts{ source: { kind, refs }, visibility, reveal?, dry_run? }(session +decisionscope; org from the principal).dry_run: truereturns the exact would-be receipt for preview — nothing is written. The bake-off / regret source is a proposal that carries the bake-off evidence (refs: [proposalId]). - List —
GET /v1/receipts(membership) lists the org's published receipts. - Revoke —
DELETE /v1/receipts/:idwrites a tombstone (<id>.revoked), deletes the published object, and records a supersedingartifactsrow; the public page then returns 410. The originalartifactsrow is immutable (supersession is a new row, not a mutation). Two independent things make a revoke stick: 1. The tombstone is authoritative./v1/r/:id,/v1/verify/:id, and the badge read the tombstone and the object concurrently and answer 410 whenever the tombstone is present — even if the object is somehow still there. So revocation does not depend on the delete succeeding. 2. The object is deleted (best-effort). Removing the payload from the store is defense in depth: a stale presignedGETor a store-level cache cannot resurface the body. If the delete fails transiently, the tombstone still guarantees410, and a later re-revoke is idempotent.
This deletes only ModelRig's own published receipt object (in the public public/receipts/* key space). It is unrelated to artifact content custody, where customer bytes under {org_id}/{artifact_id} remain write-once.
Residual limit (honest): the public /v1/r/:id JSON is served with a short cache (public, max-age=300), not an immutable one. A reader (or CDN) that cached a 200 within the last 5 minutes before a revoke may briefly still see that cached copy until the entry expires; after ≤5 min every fresh read returns 410. This is the deliberate trade for cache efficiency — a revoke is not instantaneous across already-warm downstream caches, but it is bounded to 5 minutes, and the origin object is gone immediately.
Recipient links
visibility: "recipient" publishes to one recipient: the 128-bit receipt id is the capability. No listing endpoint exposes it, so anyone holding the link can read the receipt until revoked. Used for TB-1 statements and D3 disclosures ("send this link to your customer").
Verify — by API
GET /v1/verify/:id re-fetches the published object, re-canonicalizes it, re-hashes it, and checks the signature → { valid, hash, key_id, issued_at }. Any byte changed since publication makes valid false. This route, like GET /v1/r/:id and the badge, never queries a database — it is fed by the published object alone (public = published data only).
Verify — by hand
The public keys are served at GET /.well-known/modelrig-receipts.json ({ keys: [{ key_id, public_key, alg }] }, public_key = base64 SPKI DER).
1. Fetch the receipt JSON from /v1/r/<id>. 2. Drop hash and signature; canonicalize the rest (JCS: keys sorted recursively, arrays in order, no whitespace, UTF-8). 3. sha256 the canonical bytes → compare to the receipt's hash (as sha256:<hex>). 4. Verify the Ed25519 signature.sig (base64) over the hash string against the public key whose key_id matches signature.key_id.
import { createPublicKey, verify } from "node:crypto";
const r = await (await fetch(`${BASE}/v1/r/${id}`)).json();
const { hash, signature, ...body } = r;
const canon = JSON.stringify(sortKeysRecursively(body)); // JCS-style
const recomputed = "sha256:" + createHash("sha256").update(canon, "utf8").digest("hex");
const { keys } = await (await fetch(`${BASE}/.well-known/modelrig-receipts.json`)).json();
const k = keys.find((k) => k.key_id === signature.key_id);
const pub = createPublicKey({ key: Buffer.from(k.public_key, "base64"), format: "der", type: "spki" });
const ok = recomputed === hash &&
verify(null, Buffer.from(hash, "utf8"), pub, Buffer.from(signature.sig, "base64"));
Honest limits
A published receipt proves what was measured on the stated n. It is not an audit, not a certification, and not a guarantee about future traffic. The one sanctioned line on a receipt is "measured by ModelRig — verify" — a pointer to the check, never an endorsement. Recipient links are capability URLs: anyone holding the link can read the receipt until it is revoked.