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

> Published: 2026-11-07 | Author: AgentBadge Team | Canonical: https://agentbadge.xyz/blog/supply-chain-verifiable-showcase

Part of the verifiable supply-chain series — start here: [The Bedrij Principle](https://agentbadge.xyz/blog/bedrij-principle), then [Brick #1: Typed Participants](https://agentbadge.xyz/blog/supply-chain-participant-roles), [Brick #2: Typed Evidence](https://agentbadge.xyz/blog/supply-chain-typed-evidence) and [Brick #3: Scenario Matrix](https://agentbadge.xyz/blog/supply-chain-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](/images/blog/supply-chain-verifiable-showcase-2.webp)
   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](/images/blog/supply-chain-verifiable-showcase-d1.webp)
   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](/images/blog/supply-chain-verifiable-showcase-3.webp)
   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](https://agentbadge.xyz/blog/bedrij-principle) → [Brick #1](https://agentbadge.xyz/blog/supply-chain-participant-roles) → [Brick #2](https://agentbadge.xyz/blog/supply-chain-typed-evidence) → [Brick #3](https://agentbadge.xyz/blog/supply-chain-scenario-matrix) → 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](https://agentbadge.xyz/blog/bedrij-principle). 

*Don't certify. Measure.*

---

## For AI Agents

- Companion guide: https://agentbadge.xyz/agent-guide/articles/supply-chain-verifiable-showcase
- Knowledge Index: https://agentbadge.xyz/agent-guide/
- LLM entry point: https://agentbadge.xyz/llms.txt
- Engineering services: https://agentbadge.xyz/agent-guide/team/services
