Verifying a Promethean L12 receipt chain takes three commands and about 30 seconds. The verifier is Apache-2.0, single-file, Node 18+ stdlib only, zero dependencies. A regulator running it on an air-gapped audit machine doesn't install anything from npm and doesn't talk to any Promethean infrastructure at verification time.
This post walks the full procedure with real output.
Setup
You need:
- A machine with Node.js 18 or later.
- The operator's chain (a JSONL file — one JSON object per line). The operator hands you this in their audit bundle.
- The operator's signing public key, also in the audit bundle.
That's everything. No accounts, no credentials, no install.
Step 1: Download the verifier
curl -O https://promethean.software/verify.mjs
About 16 KB on disk. You can cat verify.mjs to read the source — that's the entire verifier. No build step, no transpilation, no bundler. Apache-2.0; you can fork it into your own environment if you want.
Step 2: Get the chain
For practice, use one of the public reference chains:
curl -O https://promethean.software/paysafe-runtime-ai-receipts.jsonl
That's a synthetic-but-realistic 12-entry fraud-classifier chain. The chain is signed under the reference-deployment key (y6F3rt10CEcSakCrnJIYkGymA66x3VXm0zCWbPjRxP8=) — the verifier knows this key by default, so you can run without specifying --trusted-key.
For real operator audits, the chain will be in the audit bundle the operator gives you. Look for chain.jsonl or similar in the bundle and the workspace's public key in workspace.json.
Step 3: Run the verifier
node verify.mjs paysafe-runtime-ai-receipts.jsonl
Output:
Verifying paysafe-runtime-ai-receipts.jsonl … entries: 12 hash chain: OK signatures: 12/12 verified trust anchor: y6F3rt10CEcSakCrnJIYkGymA66x3VXm0zCWbPjRxP8= embedded key: y6F3rt10CEcSakCrnJIYkGymA66x3VXm0zCWbPjRxP8= (matches trusted) head hash: 13e4274c831dc3a2525e31fb495c34adca7bfd93cde33b8d6771e2cafe4352e5 earliest entry: 2025-05-12T11:30:00.000Z latest entry: 2025-05-12T11:30:11.000Z products: paysafe specs: paysafe-fraud-classifier models: anthropic/claude-sonnet-4-5-20250929@2026-05-01 categories: classifier:12 schema violations: 2 fallback triggers: 2 reviewer verdicts: approved:1 rejected:0 amended:1 ids contiguous: yes timestamps: monotonic malformed lines: 0 PASS (exit 0)
What every section means
Reading top-to-bottom:
entries: 12— total number of L12 receipts in the chain.hash chain: OK— every entry'sprevHashmatches the previous entry'shash, and every entry'shashfield equals SHA-256 of the entry's canonical form. If anyone edited any entry retroactively, this would say "BROKEN" with the entry id.signatures: 12/12 verified— every entry's Ed25519 signature is valid against the public key embedded in the entry'sattestationfield.trust anchor— the public key the verifier considers authoritative. Comes from--trusted-keyor the verifier's built-in default (which is the reference-deployment key).embedded key— the public key actually embedded in the chain's entries. "matches trusted" means the chain is signed by the entity we expect. "differs from trusted" is a warning, not a failure — the signature mathematically verifies under the embedded key, but a third party should investigate why the operator is using a key different from the published trust anchor.head hash— the SHA-256 of the most recent entry. This is what gets anchored to Bitcoin via OpenTimestamps. Two regulators each running the verifier against the same chain export must compute the same head hash, byte-for-byte.earliest entry / latest entry— the time-window of the chain. For a DORA incident postmortem you'd narrow this to the incident window.products / specs / models / categories— what ran during this chain's lifetime. Multiple values indicate spec rotations, model swaps, or category mix.schema violations: 2— count of entries where the model output failed validation. For a healthy AI system this should be small + non-zero (zero means the schema is too permissive; many means the model is unreliable).fallback triggers: 2— count of entries where the deterministic fallback fired. Should correlate closely with schema violations.reviewer verdicts— count of entries where a human reviewer was engaged, with the verdict distribution. For GDPR Article 22 work,rejectedrate is the operative number — zero means the reviewer is not exercising authority.ids contiguous / timestamps— soft signals. Non-contiguous IDs may indicate the chain was rotated; a non-monotonic timestamp indicates clock skew or tampering. Neither fails the verification on its own.malformed lines: 0— JSON-unparseable lines. Any non-zero count fails the verification because the chain integrity guarantee is broken — some decisions are unreadable.
What tampering looks like
Three failure modes you can reproduce yourself:
Edit a single field
sed -i 's/"latencyMs":162/"latencyMs":99999/' paysafe-runtime-ai-receipts.jsonl node verify.mjs paysafe-runtime-ai-receipts.jsonl # entries: 12 # hash chain: OK # signatures: 0/12 before break # ... # FAIL: entry 1: hash mismatch (recomputed b7e9..., claimed 9c34...)
Changing any field in any entry changes the canonical serialization, which changes the SHA-256, which the verifier detects on the first entry it sees the mismatch. Exit code 1.
Delete a middle entry
sed -i '5d' paysafe-runtime-ai-receipts.jsonl node verify.mjs paysafe-runtime-ai-receipts.jsonl # FAIL: entry 6 prevHash mismatch (expected d1ec…, got c7f7…)
Deleting an entry breaks the prevHash link of the entry that came after it. The verifier reports the first broken link.
Forge a new entry
Adding an entry with valid hash continuity requires regenerating every subsequent prevHash AND producing valid Ed25519 signatures — which requires the operator's private key. Without the key, the forgery is mathematically impossible. With the key, the forgery is possible but the operator has compromised themselves (and OpenTimestamps anchoring caps how far back the forgery can reach).
JSON output for downstream tooling
For automated audit pipelines:
node verify.mjs paysafe-runtime-ai-receipts.jsonl --json > result.json
Produces structured output suitable for ingestion into a regulator's case-management system:
{
"valid": true,
"totalEntries": 12,
"verifiedEntries": 12,
"fullyVerifiedEntries": 12,
"firstBrokenEntryId": null,
"reason": "ok",
"recomputedHeadHash": "13e4274c831dc3a2525e31fb495c34ad...",
"trustedKey": "y6F3rt10CEcSakCrnJIYkGymA66x3VXm0zCWbPjRxP8=",
"embeddedKey": "y6F3rt10CEcSakCrnJIYkGymA66x3VXm0zCWbPjRxP8=",
"embeddedKeyMatchesTrusted": true,
"malformedLines": [],
"idsContiguous": true,
"timestampsMonotonic": true,
"productIds": ["paysafe"],
"specIds": ["paysafe-fraud-classifier"],
"modelIdentities": [
"anthropic/claude-sonnet-4-5-20250929@2026-05-01"
],
"categoryCounts": { "classifier": 12 },
"schemaViolations": 2,
"fallbackTriggers": 2,
"reviewerVerdicts": {
"approved": 1, "rejected": 0, "amended": 1
},
"earliestRecordedAtIso": "2025-05-12T11:30:00.000Z",
"latestRecordedAtIso": "2025-05-12T11:30:11.000Z"
}Custom trust anchor (operator workspaces)
Operator workspaces sign their chains under their own per-workspace Ed25519 key (not the reference-deployment key). To verify their chain, pass --trusted-key:
node verify.mjs chain.jsonl --trusted-key b0CIRFxyDdIRcEOK6jwhTpRxezS7wCJESaZCTDSVVvs=
The operator's public key lives in their audit bundle's workspace.json. You're verifying two things at once: (a) the chain's internal integrity, and (b) that the chain was signed by the entity you expect. If the embedded key in the chain doesn't match what you passed, the verifier flags it — the chain might still be internally consistent, but it isn't signed by who you think.
Exit codes summary
0— chain valid. Every hash recomputes, every signature verifies, chain is unbroken.1— chain invalid. Specific failure mode is in the output's "FAIL" line.2— file unreadable or unusable. Different from "chain invalid" — this is the equivalent of "can't even start".
OpenTimestamps Bitcoin anchor verification
If the operator's chain has a .ots sidecar proof file from OpenTimestamps, you can additionally verify that the chain head was published before a given Bitcoin block height. The OpenTimestamps client tool (ots verify chain.jsonl.ots) handles this — it's a separate utility, also Apache-2.0, with its own ~100-line verifier you can read.
This is the strongest possible audit anchor: the chain could not have been retroactively constructed past the anchor's Bitcoin block, because doing so would require rewriting Bitcoin's history. For most operators this is the Enterprise-tier behaviour (15-min anchoring); lower tiers anchor hourly or daily.
Next steps
- For a deeper technical reading of the canonical form + signature math, read the verifier source — it's the documentation.
- For regulator-side concerns about audit-log standards, see traditional logging vs. cryptographic audit logs.
- For framework-specific evidence mapping, see the seven framework templates at /regulators.
Sector-specific guidance
For your industry
The verifier runs the same way regardless of vertical, but the questions a regulator asks of the chain differ by sector. The industries hub frames the audit posture per buyer-context — pick the one closest to your product.