maksa

docs · receipts

Receipts format

One receipt per finished job, written next to your outputs as receipt.json and published, redacted, at /receipts/<id>.

Fields#

fieldmeaning
idStable receipt id; encodes the minute the job finished.
customerYour redacted name: an animal and a number, stable for your account.
modelModel name as run.
tokensInput + output tokens, rounded to three significant figures.
tierDeadline you chose: 1h, 6h, 24h or 72h.
submitted_atWhen we accepted the job (UTC).
deadline_atsubmitted_at + tier.
delivered_atWhen your bucket confirmed the last output object.
regionWhere it ran. Split jobs list the region with the most GPU-hours.
gpuGPU type used.
gpu_hoursGPU-hours billed, one decimal.
costWhat you paid, USD.
would_have_costMedian of the three cheapest listed providers, on-demand, same job, at submission time.
savingwould_have_cost − cost.
deletion_atLatest 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;
}