Skip to content
· 6 min

Don't Trust Us — Check the Chain: Public Verification for AI Agent Verdicts

Every verdict ships with an on-chain receipt: GET /api/verify/:verdictId (public facade), the /verify SSR page accepting verdict id | tx hash | memo id, and @agentbadge/verify-verdict — a standalone package that re-checks the memo anchor, epoch head anchor, and full chain recompute with zero calls to our server.

AB
AgentBadge Team
Agency for the Agentic Web
Don't Trust Us — Check the Chain: Public Verification for AI Agent Verdicts

Every AgentBadge verdict can be verified independently of our server. GET /api/verify/:verdictId returns the artifact, reason codes, memo anchor, hash-chain status, and the PROOF_KINDS_V1 registry (verdict | witness | anchor-head; ?kind= narrows per layer, unknown → 400 unknown_proof_kind). The paid response itself carries an inline receipt — verify:{api,page,memoId} — with the deterministic memo id. Level-2 is npx @agentbadge/verify-verdict: proofs[] across verdict anchor, anchor-tx, anchor-head, and chain-recompute (firstBrokenSeq on tamper), exit 0/1, memo contract 0x5294E9927c3306DcBaDb03fe70b92e01cCede505. Unanchored renders pending, never valid; RPC failure is an honest rpc_unavailable error.

What happens if we lie to you? Not a bug — a lie. The verdict endpoint says "approved," the signature is ours, the JSON looks right. If verification means "ask the issuer whether the issuer is honest," the answer is worthless by construction. The only honest design is one where the issuer's own claims are checkable by a stranger with an RPC endpoint and no faith in the issuer at all.

That is what we shipped for every verdict our evaluator signs. Each paid response now carries its own verification receipt — links and a deterministic memo id — and every claim in that receipt re-verifies on-chain through a standalone package that never calls our API. If our server disappears tomorrow, the proofs still check.

A magnifying glass held over a sealed verdict document on a chain; the document's hash glows where it matches the on-chain record

How do you verify a verdict through the public API?

One GET, no auth: GET /api/verify/:verdictId returns the stored verdict artifact, machine reason_codes, the per-verdict memo anchor state (status, memoId, txHash, blockNumber), the hash-chain status (anchored/pending/disabled with a head_ref pointer for offline recomputation), and a proof_kinds registry summary. Unknown id → 404 verdict_not_found; it never answers a fake-valid.

The response is layered by design. ?kind=verdict returns only the verdict layer (artifact + memo anchor); ?kind=anchor-head returns only the chain layer; ?kind=witness answers implemented:false — the kind is declared in the registry so verifiers can feature-detect it before it ships. Ask for an unregistered kind and you get 400 unknown_proof_kind with the full known list, not a silent fallback.

Two-level verification: the public API facade vs a standalone verifier reading the chain directly — both check the same memo anchor

What does the /verify page do?

It is the same check for humans: a server-rendered page at /verify that accepts a verdict id, an anchor transaction hash, or a memo id — all 0x-prefixed 64-hex strings — and resolves which one you pasted via the anchor reverse-lookup. It renders the verdict, the reason codes, the anchor status with an explicit pending banner when the on-chain write has not landed, explorer links, and JSON-LD metadata — all without JavaScript, so it works in curl and in an agent's text browser.

Honest statuses are the point. An unanchored verdict renders "pending," never "valid." A malformed id is a 400, a well-formed unknown id is a 404, and the page tells you which of the two happened.

How do you verify without trusting AgentBadge at all?

Run the standalone package. @agentbadge/verify-verdict re-checks everything against the chain itself — the memo contract at 0x5294E9927c3306DcBaDb03fe70b92e01cCede505 on Arc, no call to our API, no account, no key:

npx @agentbadge/verify-verdict 0x<verdictId-or-txHash>
# exit 0 — every proof passed
# exit 1 — invalid, unanchored, or a typed error (rpc_unavailable)

Each run produces a proofs[] report — one entry per layer, each ok:true/false plus detail:

  • verdict — the per-verdict memo anchor exists at memoIdFor("eaas", verdictId) and its memo data equals keccak256 of the canonical artifact JSON (pass the artifact via --artifact for the tamper check).
  • anchor-tx — the memo event lives inside the transaction you supplied.
  • anchor-head — the periodic epoch anchor at memoIdFor("chain", "eaas-verdicts", epochSeq) binds (headHash, count, prevAnchoredHeadHash), chaining epochs.
  • chain-recompute — feed it the chain entries (--entries) and it folds them locally: a broken entry fails with firstBrokenSeq, a verdict absent from the entries fails honest-invalid.

Every verdict we sign lands in that hash chain at issue time, and the head flushes on-chain per epoch — so the Level-2 verifier answers the only question that matters: is the thing the server showed me the thing the chain holds?

verify-verdict 0x4af3… --artifact verdict.json --entries chain.json
# proofs: verdict ok | anchor-head ok | chain-recompute ok
# valid: true — exit 0

Terminal output: three proofs ok with a VALID badge; a second frame shows firstBrokenSeq under an INVALID badge

What is the proof-kind registry?

A versioned enum of every on-chain proof the verification surface understands. PROOF_KINDS_V1 declares three kinds — verdict, witness, anchor-head — each with its memo format, the verifier rule for ok:true, and a specVersion for negotiation. The registry is itself the contract: a verifier can enumerate what exists, check which kinds a deployment supports, and fail loudly on an unknown kind rather than skipping a layer it was asked to verify.

The witness kind is the telling entry. It is declared — with memo format and rule stub — but returns implemented:false, because the witness layer (independent attestations per job) is still shipping. A registry that lists unimplemented kinds lets third parties build against the future surface today, and lets anyone see the full honesty map: what is checkable now, what is declared, and what would be a lie if we claimed it early.

Flow diagram: verdict artifact → memo anchor and hash-chain entry → epoch head anchor; Level-1 API and Level-2 package both re-check the anchors; chain recompute catches tampering at firstBrokenSeq

The seam in one picture: a verdict writes two proofs — its own memo anchor and a chain entry — and the chain's head anchors each epoch. Level-1 is our facade over the same data; Level-2 checks both layers from the RPC directly, and the recompute fold fails at the first tampered entry.

What does "pending" mean — and why is there no fake-valid?

Because a verification surface that lies is worse than none. Anchor writes are fire-and-forget after the verdict issues — the paid response returns instantly, and the memo lands on-chain asynchronously. Until it lands, every surface says pending: the API's anchor.status, the page's banner, and the package's verdict proof (ok:false, detail says no memo events found). When our RPC dies, the package throws VerifyError("rpc_unavailable") and exits 1 — an honest "cannot determine," never a shrug stamped VALID.

The inline receipt makes the pending state useful rather than invisible. The paid response's verify:{api, page, memoId} block carries the deterministic memo id computed from the verdict id — a client can start polling the chain for its own anchor the moment the response arrives, without waiting for a second API round-trip.

This is C16 in the Arc Campaign series. Previously: The x402 Bazaar — a service catalog machines can shop. Next: Never Charged for a No — the machine-checkable refusal contract.

Links: Verify page · openapi.yaml · llms.txt · @agentbadge/verify-verdict · AgentBadge

Don't certify. Measure.