docs · receipts
Receipts format
One receipt per finished job, written next to your outputs as receipt.json and published, redacted, at /receipts/<id>.
Fields#
| field | meaning |
|---|---|
| id | Stable receipt id; encodes the minute the job finished. |
| customer | Your redacted name: an animal and a number, stable for your account. |
| model | Model name as run. |
| tokens | Input + output tokens, rounded to three significant figures. |
| tier | Deadline you chose: 1h, 6h, 24h or 72h. |
| submitted_at | When we accepted the job (UTC). |
| deadline_at | submitted_at + tier. |
| delivered_at | When your bucket confirmed the last output object. |
| region | Where it ran. Split jobs list the region with the most GPU-hours. |
| gpu | GPU type used. |
| gpu_hours | GPU-hours billed, one decimal. |
| cost | What you paid, USD. |
| would_have_cost | Median of the three cheapest listed providers, on-demand, same job, at submission time. |
| saving | would_have_cost − cost. |
| deletion_at | Latest time our staging copy of the outputs is deleted. |
Hashing#
Each field is hashed on its own: sha256("maksa:receipt:v1:" + key + "=" + value), first 12 hex characters. Numbers are written with at most four decimals, strings trimmed. The 15 digests, joined with : in the order above, hash to the root.
Because each field has its own digest, a verifier can name exactly which field changed. Because the root covers the digests, changing a digest to match an edited value breaks the root instead.
Example#
receipt.json · mk-hsts5-g5
{
"id": "mk-hsts5-g5",
"customer": "moose-10",
"model": "Qwen3 235B-A22B",
"tokens": 61799999.99999999,
"tier": "24h",
"submitted_at": "2026-11-05T01:53:44.000Z",
"deadline_at": "2026-11-06T01:53:44.000Z",
"delivered_at": "2026-11-05T19:17:07.609Z",
"region": "Iceland",
"gpu": "B200",
"gpu_hours": 5.2,
"cost": 19.53,
"would_have_cost": 33.25,
"saving": 13.72,
"deletion_at": "2026-11-08T19:17:07.609Z",
"field_digests": {
"id": "04ffbd723026",
"customer": "b193f61d81a2",
"model": "52baf2241fdb",
"tokens": "fae4a3b4af6f",
"tier": "de8bf29cad61",
"submitted_at": "f3f9b5e8b991",
"deadline_at": "ce83d082929a",
"delivered_at": "cc431a1b5a02",
"region": "8be331002204",
"gpu": "9aedf4f10aaf",
"gpu_hours": "ccded146a279",
"cost": "06a338a16a77",
"would_have_cost": "bf759475885b",
"saving": "b5f7d6b11b21",
"deletion_at": "efa310e679dd"
},
"hash": "d27cbd6c877380252c9e594bd99abc45f038444a64fbeec2bb3ce29252789abd",
"generated_at": "2026-11-05T19:18:05.220Z"
}Verifier source#
This is the file the site runs when you press Verify, read from the repository at build time. It depends only on the small SHA-256 in lib/mock/sha256.ts.
lib/mock/verify.ts
/**
* Receipt hashing and verification.
*
* Every field is hashed on its own (a 12-hex digest). The digests, in canonical
* order, are hashed together into the root printed at the bottom of the paper.
* The verifier recomputes everything from what is printed, so it can name the
* exact field that changed — or say the root itself was edited.
*/
import { sha256 } from "./sha256";
import type { ReceiptFields, VerifyCheck, VerifyResult } from "./types";
export const FIELD_ORDER: (keyof ReceiptFields)[] = [
"id",
"customer",
"model",
"tokens",
"tier",
"submitted_at",
"deadline_at",
"delivered_at",
"region",
"gpu",
"gpu_hours",
"cost",
"would_have_cost",
"saving",
"deletion_at",
];
export const FIELD_LABELS: Record<keyof ReceiptFields, string> = {
id: "receipt",
customer: "customer",
model: "model",
tokens: "tokens",
tier: "deadline tier",
submitted_at: "submitted",
deadline_at: "deadline",
delivered_at: "delivered",
region: "ran in",
gpu: "gpu",
gpu_hours: "gpu-hours",
cost: "cost",
would_have_cost: "would have cost",
saving: "saving",
deletion_at: "our copy deleted",
};
export function canonical(key: keyof ReceiptFields, value: unknown): string {
if (typeof value === "number") return `${key}=${Number(value.toFixed(4))}`;
return `${key}=${String(value).trim()}`;
}
export function fieldDigest(key: keyof ReceiptFields, value: unknown): string {
return sha256(`maksa:receipt:v1:${canonical(key, value)}`).slice(0, 12);
}
export function rootHash(digests: Record<keyof ReceiptFields, string>): string {
return sha256(FIELD_ORDER.map((k) => digests[k]).join(":"));
}
export function signFields(fields: ReceiptFields): { field_digests: Record<keyof ReceiptFields, string>; hash: string } {
const field_digests = {} as Record<keyof ReceiptFields, string>;
for (const k of FIELD_ORDER) field_digests[k] = fieldDigest(k, fields[k]);
return { field_digests, hash: rootHash(field_digests) };
}
type Verifiable = ReceiptFields & { field_digests: Record<keyof ReceiptFields, string>; hash: string };
export function verifyReceipt(r: Verifiable, now: number = Date.now()): VerifyResult {
const checks: VerifyCheck[] = [];
const mismatches: string[] = [];
for (const k of FIELD_ORDER) {
const expected = r.field_digests?.[k];
const got = fieldDigest(k, r[k]);
const ok = expected === got;
checks.push({ field: k, ok, detail: ok ? got : `printed ${String(r[k])} hashes to ${got}, receipt says ${expected ?? "nothing"}` });
if (!ok) mismatches.push(k);
}
const root = r.field_digests ? rootHash(r.field_digests) : "";
const rootOk = root === r.hash;
checks.push({ field: "hash", ok: rootOk, detail: rootOk ? root : `field digests hash to ${root.slice(0, 16)}…, printed root is ${String(r.hash).slice(0, 16)}…` });
if (!rootOk) mismatches.push("hash");
const sum = Math.round((Number(r.would_have_cost) - Number(r.cost)) * 100) / 100;
const sumOk = Math.abs(sum - Number(r.saving)) < 0.006;
checks.push({ field: "arithmetic", ok: sumOk, detail: sumOk ? `${r.would_have_cost} − ${r.cost} = ${r.saving}` : `${r.would_have_cost} − ${r.cost} = ${sum.toFixed(2)}, not ${r.saving}` });
if (!sumOk && !mismatches.includes("saving")) mismatches.push("saving");
const met = Date.parse(r.delivered_at) <= Date.parse(r.deadline_at);
checks.push({ field: "deadline", ok: met, detail: met ? "delivered before deadline" : "delivered after deadline" });
if (!met && !mismatches.includes("delivered_at")) mismatches.push("delivered_at");
return { ok: mismatches.length === 0, checked_at: new Date(now).toISOString(), root, mismatches, checks };
}
/** Parse a user-edited string back into the field's type before verifying. */
export function coerceField(key: keyof ReceiptFields, raw: string): string | number {
const numeric: (keyof ReceiptFields)[] = ["tokens", "gpu_hours", "cost", "would_have_cost", "saving"];
if (numeric.includes(key)) {
const n = Number(raw.replace(/[$,\s]/g, ""));
return Number.isFinite(n) ? n : raw;
}
return raw;
}