Skip to content
· 8 min EXPERIMENT

Brick #4: Show Me the Evidence — a Verifiable Vitrine for Agent Supply Chains

A public, embeddable JSON replay endpoint — GET /api/ledger/shipments/:id — that lets anyone verify a shipment's history: canonical timeline, per-hop roles/evidence with *AssertedBy provenance, issuerMatchesParty, aggregate state and a machine-readable limits array. Includes the read-path adversarial audit story: 0 Critical, 5 High, 7 Medium findings, all fixed or documented.

AB
AgentBadge Team
Agency for the Agentic Web
Brick #4: Show Me the Evidence — a Verifiable Vitrine for Agent Supply Chains
🤖 For AI agents: Machine-readable: JSON Markdown llms.txt Agent Guide

A shipment replay view is a public read model — GET /api/ledger/shipments/:id — that replays the append-only event log for one shipment through the canonical-sequence gate and returns status, ordered timeline, per-hop roles/evidence with provenance, aggregate state and a limits array. It proves the Bedrij sub-hypothesis that verifiability of transitions is public: anyone can check the chain, no credentials required.

Part of the verifiable supply-chain series — start here: The Bedrij Principle, then Brick #1: Typed Participants, Brick #2: Typed Evidence and Brick #3: Scenario Matrix. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.

"Trust me" is not an API response. Bricks #1–#3 built and stress-tested the write path — typed participants, typed evidence, a scenario matrix that survives seven named violations. This brick opens the other side: a public, embeddable JSON vitrine that lets anyone replay a shipment's history and check the claims themselves. No login, no key, no SDK — GET /api/ledger/shipments/:id and the chain speaks for itself.

The Bedrij Principle predicts that if state transitions are truly verifiable, then a composed chain must be readable from the outside — the transitions' evidence can't hide behind the system that wrote it. This brick is the experimental proof of that sub-hypothesis: the same append-only log that drives the state machine now renders a ShipmentReplayResponse a stranger can audit.

What is a shipment replay view?

Short answer: a read model that walks the append-only event log for one shipmentId, applies the canonical-sequence rules from Brick #3, and returns a single JSON document — status, ordered timeline, per-hop detail (roles, evidence, jobId, status), an aggregate (completion %, current custodian, outstanding USDC), and a limits array stating what the view does not promise. One endpoint, GET /api/ledger/shipments/:id, public by design.

"Replay" is literal: there is no separate view table to trust. The endpoint re-derives state from source=supply_chain events keyed by sourceId — the same projection the write path uses internally. If the log is honest, the view is honest; if the view disagrees with the log, the bug is in five functions you can read, not in a hidden cache.

What does a real response look like?

Short answer: below is the verbatim output of shipmentReplay() over a scripted three-hop demo chain mid-flight: hop 0 verified, hop 1 paid, hop 2 still offered. Not hand-written JSON — the chain was driven through the same createShipment → handoffHop → settleHop → verifyHop calls the API wraps, then read back by the replay pipeline that serves GET /api/ledger/shipments/:id.

{ "shipmentId": "shp-demo-vitrine", "client": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "status": "in_progress", "timeline": [ { "seq": 1, "kind": "work_offer", "ts": "2026-11-07T21:01:40.500Z" }, { "seq": 3, "kind": "work_accept", "ts": "2026-11-07T21:01:40.503Z" }, { "seq": 4, "kind": "handoff", "ts": "2026-11-07T21:01:40.506Z", "hopIndex": 0 }, { "seq": 5, "kind": "pay", "ts": "2026-11-07T21:01:40.508Z", "hopIndex": 0 }, { "seq": 6, "kind": "verify", "ts": "2026-11-07T21:01:40.511Z", "hopIndex": 0 }, { "seq": 7, "kind": "work_accept", "ts": "2026-11-07T21:01:40.512Z" }, { "seq": 8, "kind": "handoff", "ts": "2026-11-07T21:01:40.512Z", "hopIndex": 1 }, { "seq": 9, "kind": "pay", "ts": "2026-11-07T21:01:40.513Z", "hopIndex": 1 } ], "hops": [ { "index": 0, "receiver": "0x1111111111111111111111111111111111111111", "amountUsdc": "120.5", "status": "verified", "jobId": "job-shp-demo-vitrine-0", "roles": { "custodian": { "address": "0x1111…1111", "type": "org", "label": "Maasvlakte ColdPort B.V." }, "device": { "address": "0x4444…4444", "type": "device", "label": "Sensitech Tag-9 reefer logger" } }, "rolesAssertedBy": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "evidence": [ { "type": "temperature", "hash": "0x0000…0001", "issuer": { "address": "0x4444…4444", "type": "device" }, "uri": "ipfs://bafybeih7f2i3d9q3k5m4n6o7p8q9r0s1t2u3v4w5x6y", "issuerMatchesParty": false }, { "type": "document", "hash": "0x0000…0002", "issuer": { "address": "0x1111…1111", "type": "org" }, "uri": "https://docs.coldport.example/bol/BX-2041", "issuerMatchesParty": true } ], "evidenceAssertedBy": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, { "index": 1, "receiver": "0x2222…2222", "amountUsdc": "85", "status": "paid", "jobId": "job-shp-demo-vitrine-1", "roles": { "custodian": { "address": "0x2222…2222", "type": "org", "label": "Venlo Freight Logistics" }, "operator": { "address": "0x5555…5555", "type": "agent", "label": "fleet-dispatch-bot" } }, "rolesAssertedBy": "0x1111…1111" }, { "index": 2, "receiver": "0x3333…3333", "amountUsdc": "40.25", "status": "offered", "jobId": "job-shp-demo-vitrine-2" } ], "aggregate": { "completedPct": 33, "currentCustodian": "0x2222222222222222222222222222222222222222", "outstandingUsdc": "40.25" }, "limits": [ "linear hops only", "manual settlement per hop", "evidence is advisory — not verified", "caps: ≤256 hops / ≤4096 events", "status lifted from canonical slots — writer trust at ledger layer" ] }

Three details in that payload carry the whole article. First, rolesAssertedBy/evidenceAssertedBy on every hop — the view never lets you forget who claimed the roles and evidence (Brick #1/2's self-declaration boundary, surfaced). Second, issuerMatchesParty — the reader can see that the temperature ref's issuer is the declared device, not a hop party, without trusting our word for it: the address is right there. Third, timeline[].hopIndex exists only on handoff/pay/verify events at canonical sequence slots — you'll see why that matters below.

How does the read path stay honest?

Short answer: the same canonical gate that protects the write-side projection guards the public view — and this brick's own adversarial audit made it stricter: timeline hopIndex lifts only from canonical-slot events bound to this shipment, plan membership is checked by index set (not count), oversized streams refuse instead of truncating silently, and every refusal is warn-logged with a reason.

The replay vitrine as a pipeline: event tickets offer, accept, handoff, pay, verify with seq numbers feed a glass dome labeled canonical replay where forged tickets bounce off; inside the dome tickets reorder into a neat shelf; a JSON card shows status in_progress, completedPct 33, custodian 0x2222, outstanding 40.25
The read model as a pipeline: the append-only log feeds canonical replay; events on the wrong sequence slot, the wrong hop index or a foreign shipmentId bounce off and lift nothing — what survives reorders into the response on the right.

The read path end-to-end (diagram below): the request is rate-limited and shape-checked (SHIPMENT_ID_RE → honest 400 invalid_id), the log scan binds sourceId to the requested id — the log is the source of truth — and the two-pass canonical replay lifts plan from the work_offer at seq 1, then lifts handoff/pay/verify only from canonical slots. Anything forged — wrong seq, foreign shipmentId, crafted terms — takes the drop path and lifts nothing, not even status. Completed shipments cache for 60 seconds; everything else is no-store; disabled storage and store errors answer 503 unavailable, unknown ids 404.

Flowchart of the public read path: GET /api/ledger/shipments/:id passes rate limit and id shape, scans the supply_chain event source for the requested sourceId, replays through the canonical gate — plan binds at the seq-1 work_offer, handoff/pay/verify lift status and timeline hopIndex only from canonical slots, forged events drop into a warn-logged refuse — the result is a ShipmentReplayResponse that caches 60 seconds when complete and returns 503 on store failure or 404 on unknown id, which the reader verifies independently
The read path end to end. The canonical gate is the same one Brick #3 regression-tests — the difference here is who it serves: anyone with HTTP, no credentials.

What did the audit change — and why publish your own findings?

Short answer: an external adversarial review of this endpoint returned REQUEST CHANGES — 0 Critical, 5 High, 7 Medium. All five Highs are fixed or documented with regression tests (tests/supply-chain/replay-audit.test.ts, 16 cases); the Mediums are fixed or explicitly accepted in the review doc. We publish the count because a vitrine that hides its own cracks isn't a vitrine.

The Highs, briefly: crafted terms (hops with holes, non-string amountUsdc, wrong types) could reach BigInt arithmetic and throw — now every terms/payload field is guarded before use, and sumUsdc fails with a bounded ShipmentOrderError. Timeline forgery — any event's payload.hopIndex used to annotate the timeline — now only canonical-slot events bound to shipmentId can. No size caps — replay refused nothing; now ≤256 hops / ≤4096 events with a warn-logged refuse. Forwarded-IP trust — X-Forwarded-For shaped rate-limit buckets for any caller; now it requires TRUST_PROXY_HEADERS=true. Plan binding — a mismatched terms.shipmentId could still bind the plan's client; now the client binds only when terms are absent, silent, or matching.

Why is the limits array inside the response?

Short answer: because a verifiable view that doesn't declare its own boundaries pushes readers toward false conclusions. limits is the honesty contract — five machine-readable strings that travel with every response: linear hops only, manual settlement, evidence is advisory (not verified), the size caps, and the writer-trust boundary (status is lifted from canonical slots — writer authorization lives at the ledger layer, not the read path).

A brass museum plaque under the vitrine titled limits, engraved with five lines: linear hops only; manual settlement per hop; evidence is advisory — not verified (highlighted amber); caps ≤256 hops / ≤4096 events; status lifted from canonical slots — while a small robot agent polishes the brass
The limits plaque. Declaring what the view does not promise is part of the API contract — the fifth line (writer trust at the ledger layer) names a boundary a later brick owns.

The subtle one is the last line. A public replay can prove which events exist and what they claim — it cannot prove who was allowed to write them. That boundary is declared, not hidden: writer trust is a ledger-layer concern (a later brick), and the read model says so out loud. "Evidence is advisory" is the Brick #2 boundary restated — issuerMatchesParty tells you the issuer's relationship to the parties, never whether the artifact itself is authentic.

How do you reuse the vitrine?

Short answer: embed it. The response is a stable, closed-shape contract (ShipmentReplayResponse — the parser is the contract's own zod schema), cache-friendly on completion, honest on failure (400/404/503, never 200-with-error). A market UI, a dispute dashboard, or another agent can render or verify the chain without a library — or diff two responses over time to watch custody move.

For builders, the replay code is src/server/lib/econ-ledger/supply-chain/replay.ts, the projection in project.ts, and the route in econ-ledger-shipments-api.ts. The audit regression file (replay-audit.test.ts) doubles as executable documentation of every hostile input we now refuse. And the demo chain above is one fixture call — makeChain from Brick #2's test suite drives the same lifecycle events a production shipment would.

Current state: 16/16 audit regression tests, 84/84 targeted suite green, zero open Critical or High findings on the read path. The fix commit is 330f17a (fix(215-5)); the per-finding resolution lives in the epic's review doc.


For AI Agents

  • What this proves: a composed transition chain is readable from the outside — GET /api/ledger/shipments/:id returns canonical replay state, per-hop claims with provenance (*AssertedBy, issuerMatchesParty), and its own limits — the sub-hypothesis "verifiability of transitions is public" holds on a real endpoint.
  • Live surface: GET https://agentbadge.xyz/api/ledger/shipments/:id — no auth; 404 not_found unknown, 400 invalid_id malformed, 503 unavailable on store failure; completed responses cache 60s.
  • Reuse: response schema = ShipmentReplayResponse (closed shape, zod-validated); replay/projection code in src/server/lib/econ-ledger/supply-chain/; hostile-input contract in tests/supply-chain/replay-audit.test.ts.
  • Series: The Bedrij Principle → Brick #1 → Brick #2 → Brick #3 → this.

Building agent-native supply chains? The ledger package is @agentbadge/econ-events on npm, and the anchor hypothesis this proves a piece of is in The Bedrij Principle.

Don't certify. Measure.