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 one byte and the check fails.
Get one
In the web app, open a run and download its receipt. 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/v1
| Field | What it holds |
|---|---|
format | Always vvdex.receipt/v1 for this version. |
run_id, name | The run. |
created_at, started_at, finished_at | When the run was created, started and finished. |
platform_version | The VVDex version that ran it. |
forge or dataset | For a Forge exam: exam, version and attempts; each result’s grade carries the digest of the sealed Forge record behind it. For a dataset: its name, row_count and content_digest. |
models, judge | Each model’s public identity: provider, model id, display name and self_reported. Never a key, a connection id or an address; a custom endpoint shows only its host. |
params, graders | Temperature, token limit, system prompt, repeats, and the graders with their settings (protected grader settings appear only as hashes). |
summary | Per model: results, cases scored, passed and failed, pass rate, 95% interval, errors by kind, cost (null when unknown) and latency. Optional: by_difficulty, the same counts and interval per difficulty tier ({tier: {n_scored, n_passed, pass_rate, wilson_low, wilson_high, errors}}), present only when the run’s questions name a difficulty; a question without one counts as unspecified. Receipts made before this field existed simply do not have it, and still verify. |
results | Every result: the case key, the messages the model received, the expected answer, the model’s output, the grades, the repeat number, latency and tokens — plus its model index, status, error kind and score, and, in a run with difficulty tiers, its difficulty. Those extra fields are not in the leaf but are covered by the digest and signature. |
leaves | One SHA-256 per result, in result order, over {case_key, input, expected, output, grades, model, repeat, latency_ms, tokens}. |
leaves_root | SHA-256 of the leaf hashes concatenated as hex text. |
key_id, signature_alg | Which VVDex key signed it, and ed25519. |
digest | SHA-256 (hex) of the receipt’s canonical JSON without digest and signature. |
signature | Ed25519 signature over the digest’s hex text, base64. |
Canonical JSON means: keys sorted, separators , and : with no spaces, UTF-8 text without \u escapes, no NaN.
Because a receipt contains the prompts and answers of the run, share it only where you would share the run itself.
Verify online
The public check needs no account. It verifies the digest and the signature, recomputes every leaf and the root, checks that each model’s counts and interval (and, when present, its per-difficulty breakdown) follow from the results, and re-grades every deterministic grade it can recompute.
$ curl -s https://vvdexops.com/api/v1/receipts/verify \
-H "Content-Type: application/json" --data @receipt.json
# → {"valid": true | false, "checks": [{"check", "ok", "detail"}, …]}
$ vvdex receipt verify receipt.json # exit code 0 = valid, 1 = not validOr paste it into Check a receipt.
Verify offline, with the public key
You do not have to ask VVDex to check a signature. Save the public key once; after that, checking needs no network.
$ pip install "vvdex[offline] @ https://vvdexops.com/downloads/vvdex-0.2.0-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 Python and the cryptography package:
import base64, hashlib, json
from cryptography.hazmat.primitives.serialization import load_pem_public_key
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
def sha256(data):
return hashlib.sha256(data).hexdigest()
receipt = json.load(open("receipt.json", encoding="utf-8"))
# 1. the digest covers everything except digest and signature
body = {k: v for k, v in receipt.items() if k not in ("digest", "signature")}
assert sha256(canonical(body)) == receipt["digest"], "changed after signing"
# 2. every result matches its leaf, and the leaves add up to the root
fields = ("case_key", "input", "expected", "output", "grades", "model", "repeat", "latency_ms", "tokens")
for result, leaf in zip(receipt["results"], receipt["leaves"], strict=True):
assert sha256(canonical({k: result.get(k) for k in fields})) == leaf, "a result was edited"
assert sha256("".join(receipt["leaves"]).encode()) == receipt["leaves_root"]
# 3. VVDex signed that digest (raises InvalidSignature if not)
key = load_pem_public_key(open("vvdex-receipts.pem", "rb").read())
key.verify(base64.b64decode(receipt["signature"]), receipt["digest"].encode())
print("signed by VVDex and unchanged")The offline check proves the receipt is exactly what VVDex signed. Re-grading answers needs the graders, which run on VVDex’s servers; that part is the online check.
What a receipt is not
A receipt describes one run: these questions, these answers, these grades, on that date. 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.