You do not have to trust this service. Every score receipt can be verified
independently, offline, with any standard Ed25519 library — you need only the
content, the receipt, and the service's public key (GET /v1/public-key).
The signature is computed over a small set of signed fields — note the score and rubric_version are inside the signature, so neither can be altered without breaking it:
signed_fields = {
"content_hash": SHA-256(content) as lowercase hex,
"score": <integer 0-100>,
"rubric_version": "v1",
"receipt_id": <uuid>,
"timestamp": <iso-8601 utc>
}
A receipt binds: a tamper-evident hash of the scored content, the score, the rubric version it was scored under, a unique id, and a service-set time.
Verifying is two independent checks — a receipt is valid only if both pass:
- Hash check —
SHA-256(your content) == receipt.content_hash. - Signature check — the Ed25519 signature is valid over
signed_fieldsunder the service public key.
What this does not prove: that the score is "correct" in any absolute sense. It proves the service issued this score for this content under this rubric version. The rubric is public (quality-rubric.md) and an LLM judgment against it — see the rubric's "Limits" section.
signed_fields is serialized deterministically: object keys sorted ascending
(recursively), no insignificant whitespace, primitives encoded as
JSON.stringify encodes them. The signed/verified bytes are the UTF-8
encoding of that string.
function canonicalize(value) {
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
const keys = Object.keys(value).sort();
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(value[k])).join(",") + "}";
}A self-contained verifier (no dependency on this service's code) ships as
scripts/verify-receipt.mjs:
import * as ed from "@noble/ed25519";
import { webcrypto } from "node:crypto";
function canonicalize(value) { /* as above */ }
async function sha256Hex(str) {
const d = await webcrypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
return ed.etc.bytesToHex(new Uint8Array(d));
}
export async function verifyScoreReceipt(content, receipt, publicKeyHex) {
const hashMatch = (await sha256Hex(content)) === receipt.content_hash;
const payload = new TextEncoder().encode(canonicalize({
content_hash: receipt.content_hash,
score: receipt.score,
rubric_version: receipt.rubric_version,
receipt_id: receipt.receipt_id,
timestamp: receipt.timestamp,
}));
const sigValid = await ed.verifyAsync(
ed.etc.hexToBytes(receipt.signature), payload, ed.etc.hexToBytes(publicKeyHex),
);
return { valid: hashMatch && sigValid, hashMatch, sigValid };
}scripts/verify-receipt.mjs includes a self-check that proves the independent
verifier agrees with the service's own free POST /v1/verify, including flipping
to invalid when the score or content is tampered. To avoid a paid
/v1/score call, the self-check mints a receipt locally with the service signing
key from .dev.vars (the same key the running server publishes at
/v1/public-key), then cross-checks it both ways:
node scripts/verify-receipt.mjs http://localhost:8787
# valid receipt -> independent: valid:true | server /v1/verify: valid:true OK
# tampered score (+9)-> independent: valid:false | server /v1/verify: valid:false OK
# tampered content -> independent: valid:false | server /v1/verify: valid:false OK
# public key matches signer OK
# SELF-CHECK PASSEDBecause the live service's verification (its real product code) and an independent re-implementation agree on every case, the "verify it yourself" claim is demonstrable, not just asserted.
test/receipt.test.ts exercises this exact algorithm: unmodified → valid; a
forged score → invalid (signature covers the score); tampered content →
invalid; wrong key → invalid. Run with npm test.