DEV Community

Paul Spread
Paul Spread

Posted on Originally published at agentbadge.xyz

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

This article originally appeared on the AgentBadge blog.

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.

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.

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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 API · Verify page · openapi.yaml · llms.txt · @agentbadge/verify-verdict · AgentBadge

Don't certify. Measure.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev •

Making the receipt independently verifiable is the right goal. I would also bind the verifier to a canonical payload hash and an explicit chain/network identifier, then test verification against a deliberately altered field. That avoids a common ambiguity where a valid signature is checked over a differently serialized or differently scoped verdict.