Proof · Receipts
Receipts
Every evaluation run is sealed in a receipt: a JSON document that records what was asked, what came back and how it was graded, signed by VVDex. Change any value and the check fails.
Report, receipt, check
A report is the readable summary of a run. A receipt is the signed, machine-readable evidence of exactly what VVDex recorded. Checking a receipt confirms it has not been altered since VVDex signed it. Every number in a report comes from the run’s sealed receipt.
Get one
In the web app, open a run and choose Receipt: it shows what the receipt records and checks it as you open it. Receipt file saves the file exactly as VVDex signed it. From code: GET /api/v1/evals/{id}/receipt, client.evaluation_receipt(id), or share the run and hand over the link.
The format: vvdex.receipt/v2
Every field is committed by VVDex’s signature: the digest covers the whole receipt except the digest and the signature themselves. Each result is also committed by its own leaf. The checker recomputes what can be recomputed. What rests on VVDex’s own records is said as such.
| Field | What it holds | Signed · recomputed |
|---|---|---|
format | vvdex.receipt/v2. | signed |
run_id, name, created_at, started_at, finished_at, platform_version | The run and when it ran. | signed |
forge or dataset | For a Forge exam: exam, version, attempts. For a dataset: its name, row_count and content_digest. | signed |
models, judge | Each model’s public identity: provider, model id, display name, self_reported. Never a key, a connection id or an address; a custom endpoint or app shows only its host. | signed · recomputed: each result names its listed model |
params, graders | Temperature, token limit, system prompt, repeats; the graders with their settings (protected settings only as hashes). | signed |
results | Every result: case key, the messages the model received, the expected answer, the output, the grades, repeat, latency, tokens, model index, status, error kind, score, cost; in a run with difficulty tiers, its difficulty. A Forge attempt’s grade carries its certified verdict, steps, containment, what VVDex adapted for the model, the exam’s certification (goldPassRate: the reference solution’s pass rate, expected 1; baselinePassRate: the untouched code’s, expected 0; attack probes blocked) and the digest of its sealed record (recordSha256). | signed, and by its own leaf · recomputed: each Forge outcome from its certified verdict |
leaves, leaves_root | One SHA-256 per result, in result order (case position, then model, then repeat), over the canonical JSON of the whole result; the root is SHA-256 of the leaf hashes joined as hex text. | signed · recomputed from the results |
summary | Per model: results, scored, passed, failed, pass rate, 95% interval, mean score, errors by kind, cost (null when unknown), latency; optionally by_difficulty. | signed · recomputed: counts, rates, intervals. Cost and latency are signed as VVDex measured them |
key_id, signature_alg | Which VVDex key signed it, and ed25519. | signed |
digest | SHA-256 (hex) of the receipt’s canonical JSON without digest and signature. | the seal · recomputed from the receipt |
signature | Ed25519 over the digest’s hex text, base64. | the seal · checked with VVDex’s public key |
Unsigned: nothing but the seal itself. Never in a receipt: keys, connection ids or addresses, your workspace or email, error messages (only their kind), a model’s conversation, an attempt’s evidence files, an exam’s task files, hidden tests, reference solution or grader.
Canonical JSON is RFC 8785, the JSON Canonicalization Scheme: keys sorted by their UTF-16 code units, no whitespace, strings minimally escaped, and every number written as JavaScript writes it (1.0 and 1 are both 1). A receipt therefore keeps its digest through any tool that keeps its values: pretty-printed or compact, keys in any order, \u escapes, a browser’s parse and re-save. Three rules follow. The order of results is part of the receipt. A field set to null and a missing field are different receipts. Text is compared exactly, without Unicode normalisation.
Because a receipt contains the prompts and answers of the run, share it only where you would share the run itself.
Check online
The public check needs no account: paste or upload at Check a receipt, or send the file as it is. The answer is one verdict: authentic, altered (VVDex’s signature is genuine but the content changed), forged (the signature is not VVDex’s), unverifiable (signed with a key this server does not hold), malformed or unsupported. Each check says whether it is a hash or signature, a recomputation or a structure check, and a valid receipt comes with what it proves by itself and what rests on VVDex’s own records.
$ curl -s https://vvdexops.com/api/v1/receipts/verify \
-H "Content-Type: application/json" --data-binary @receipt.json
# → {"valid": true | false, "verdict": "authentic" | "altered" | …, "headline", "explanation",
# "checks": [{"check", "ok", "detail", "kind"}, …], "trust": {"proves": […], "vouched": […]}}
$ vvdex receipt verify receipt.json # exit code 0 = valid, 1 = not validCheck offline, with the public key
You do not have to ask VVDex. The hash rules are one small file, vvdex/receipt_core.py in the SDK. VVDex seals every receipt with that very file.
$ pip install "vvdex[offline] @ https://vvdexops.com/downloads/vvdex-0.3.1-py3-none-any.whl"
$ vvdex receipt public-key > vvdex-receipts.pem # or GET /api/v1/receipts/public-key
$ vvdex receipt verify receipt.json --offline --public-key vvdex-receipts.pemOr with nothing but a JavaScript runtime. Its own JSON writer is the canonical one, so sorting the keys is all that is added:
const crypto = require("crypto"), fs = require("fs");
const jcs = (v) => Array.isArray(v) ? "[" + v.map(jcs).join(",") + "]"
: (v !== null && typeof v === "object") ? "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}"
: JSON.stringify(v);
const sha = (s) => crypto.createHash("sha256").update(s, "utf8").digest("hex");
const r = JSON.parse(fs.readFileSync("receipt.json", "utf8"));
// 1. the digest covers everything except digest and signature
const body = Object.fromEntries(Object.entries(r).filter(([k]) => k !== "digest" && k !== "signature"));
if (sha(jcs(body)) !== r.digest) throw new Error("changed after signing");
// 2. every result matches its leaf, and the leaves add up to the root
r.results.forEach((x, i) => { if (sha(jcs(x)) !== r.leaves[i]) throw new Error(`result ${i + 1} was edited`); });
if (sha(r.leaves.join("")) !== r.leaves_root || r.leaves.length !== r.results.length) throw new Error("leaves");
// 3. VVDex signed that digest
const key = crypto.createPublicKey(fs.readFileSync("vvdex-receipts.pem"));
if (!crypto.verify(null, Buffer.from(r.digest), key, Buffer.from(r.signature, "base64"))) throw new Error("not VVDex's");
console.log("signed by VVDex and unchanged");The offline check proves the receipt is exactly what VVDex signed. Recomputing the counts is plain arithmetic on the results. Re-grading answers needs VVDex’s graders: that part is the online check.
Older receipts: vvdex.receipt/v1
Receipts issued on 23 and 24 September 2026 are v1, and they still check. v1’s canonical JSON was Python’s json.dumps, which writes 1.0 and 1 differently. Until 24 September 2026, VVDex’s own Receipt button and receipt checker re-saved receipts in the browser, which writes 1.0 as 1. That made genuine receipts fail. The checker now takes a v1 file as it is. If that does not match, it writes those whole numbers back as decimals (1 → 1.0) in the fields VVDex wrote as decimals, and tries again. A match there matches the digest VVDex signed, so it proves the same content, value for value. v1 leaves cover {case_key, input, expected, output, grades, model, repeat, latency_ms, tokens}; the rest of a v1 result is covered by the digest.
What a receipt is not
A receipt describes one run: these questions, these answers, these grades, on that date. It proves what VVDex recorded and signed. It does not prove by itself that a hidden grader ran as described: that rests on VVDex’s certification of the exam, and each attempt’s sealed record is committed by its digest. It is not a certificate that a model is fit for any purpose, and it says nothing about how the model behaves on other inputs. For models reached through a custom address or an HTTP app, the model’s name is what its owner reported.