DEV Community

Cover image for Brick #4: Show Me the Evidence - a Verifiable Vitrine for Agent Supply Chains
Paul Spread
Paul Spread

Posted on Originally published at agentbadge.xyz

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

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.

A glass museum vitrine holding a three-crate supply chain - two crates glow emerald (verified, paid), the third is dim and still offered; a robot agent holds a magnifying glass over the chain

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.

Meet the shipment: a cold-chain run in plain words

Picture a delivery any logistics manager would recognize. A biotech buyer contracts a refrigerated container of temperature-sensitive product at the Maasvlakte cold port in Rotterdam. The container has three legs: the port hands it to a trucking company, the trucker hands it to a pharma warehouse in Venlo. Each leg repeats the same three-step ritual - hand over custody, pay for the leg, verify the handoff. A reefer logger on the container writes temperature evidence, the port writes a bill of lading, the truck's telematics write GPS traces, and the dispatcher bot signs the acceptance.

In the demo chain below, leg one is finished - custody moved from port to trucker, 120.5 USDC paid, verification passed. Leg two is done and paid (85 USDC). Leg three is only offered: the warehouse hasn't accepted yet, 40.25 USDC still outstanding. Everything a courier's tracking page would show - plus the one thing it never shows: anyone can re-check the math.

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

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.

Read it like a tracking page, then like an auditor. timeline is the event feed - the offer at seq 1, then accepts, handoffs, payments, and the verify at seq 6 that closed leg one. hops[].roles names who was involved - the port as custodian, the reefer logger as a device - and hops[].evidence lists their artifacts with *AssertedBy provenance: who claimed it, not who proved it. aggregate is the headline a dashboard would print: 33% complete, the trucker currently holding custody, 40.25 USDC left to pay.

If a dispute lands tomorrow - "the container arrived warm, who pays?" - this JSON is the document both sides start from, and neither of them had to produce it. That is the difference between a tracking page and a vitrine: the tracking page is the courier's word; this is the courier's arithmetic, checkable by a stranger.

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 feed a canonical replay dome where forged tickets bounce off; a JSON card shows status in_progress, completedPct 33

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.

The everyday version of this check: recomputing a bank statement from the receipts instead of trusting the courier's tracking screenshot. A forged "handoff" ticket - wrong sequence, wrong shipment, wrong slot - is a receipt that doesn't belong in the pile, so replay drops it on the floor and logs why.

Flowchart of the public read path: GET → rate limit + id shape → log scan → canonical replay → ShipmentReplayResponse; forged events drop warn-logged; completed → max-age 60; 503/404 honest failures

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 with five engraved lines: linear hops only; manual settlement per hop; evidence is advisory - not verified; caps ≤256 hops / ≤4096 events; status lifted from canonical slots

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. It is the same honesty as a lab report that prints its detection limits next to the result - the number means more when the instrument admits what it cannot see.

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 the rest of us the use cases are mundane and real: an insurer checking a cold-chain claim, a buyer opening a dispute, a freight marketplace rendering custody history before accepting a bid - none of them needs an account with us.

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.

Top comments (0)