← Resources/Blog· Substrate · 9 min read

How to verify an L12 receipt chain — a regulator's walkthrough

Step by step: download the verifier, download a chain, run it, read the output. What every section means. What tampering looks like. What exit codes 0/1/2 actually tell you about the operator's audit trail.

Published 2026-05-15

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's prevHash matches the previous entry's hash, and every entry's hash field 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's attestation field.
  • trust anchor — the public key the verifier considers authoritative. Comes from --trusted-key or 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, rejected rate 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.

All 12 industries →

Frequently asked questions

Do I need to install anything to verify a Promethean L12 chain?

No. The verifier is a single ~500-line Node 18+ stdlib file with zero dependencies, served at https://promethean.software/verify.mjs under Apache-2.0. You need Node installed (which most regulator-side audit environments already have); nothing else. No npm install, no package manager, no network access to Promethean infrastructure at verification time.

What does exit code 0 mean?

Every entry's hash recomputes to the value claimed, every Ed25519 signature verifies against the embedded public key, the chain is unbroken (each entry's prevHash matches the previous entry's hash), and no lines are malformed. Soft warnings (non-contiguous IDs, non-monotonic timestamps) are surfaced but do not fail the verification — they're signal for the auditor to review, not a verification failure.

What does exit code 1 mean?

The chain failed verification. Specific failure modes: (a) a recomputed hash doesn't match the entry's claimed hash field (entry was edited); (b) an Ed25519 signature doesn't verify (entry was edited or wrong key embedded); (c) prevHash mismatch (chain is broken — an entry was inserted, deleted, or reordered); (d) malformed JSON on one or more lines. The verifier reports which entry and why, so the auditor knows exactly what to flag.

Can I verify chains produced by a different operator's workspace?

Yes — that's the design. The verifier is universal; it works against any chain that follows the L12 canonical form. You'll want to pass --trusted-key with the operator's public key (which they publish in their audit bundle's workspace.json). Without --trusted-key, the verifier uses Promethean's reference-deployment trust anchor (y6F3rt10...), which won't match the operator's workspace-specific signing key.

What if the operator's chain is too big to inspect by eye?

verify.mjs handles chains of any size. It walks the chain entry-by-entry, recomputes each hash and signature, and only fails fast on the first integrity violation. For chains with millions of entries the run takes proportionally longer (Ed25519 verification is fast — ~5,000 signatures per second on a modern laptop). For random-sample inspection by a human auditor, the JSON output mode (--json) produces structured stats — entries-per-spec, model-identity distribution, schema-violation counts, reviewer-verdict distribution — that scale to any chain size.