Skip to content
· 7 min EXPERIMENT

Brick #2: A Link Is Not a Fact — Typed Evidence in a Verifiable Supply Chain

A handoff event says custody moved — but where is the proof? Brick #2 adds typed evidence references {type, hash, issuer, uri?} to the handoff payload: proof that travels with the transition, replayed with evidenceAssertedBy + issuerMatchesParty provenance. Includes the adversarial AI audit story — 0 Critical, 2 High, 7 Medium findings, all fixed or documented.

AB
AgentBadge Team
Agency for the Agentic Web
Brick #2: A Link Is Not a Fact — Typed Evidence in a Verifiable Supply Chain

Typed evidence is an optional list of {type, hash, issuer, uri?} references on a handoff event — seven write types, 1–32 refs, URI allowlist — replayed into per-hop state with evidenceAssertedBy and issuerMatchesParty provenance. A hash proves bytes existed, not that they are true; advisories warn, never block. Shipped after an adversarial AI audit (2 High, 7 Medium, all resolved).

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

A handoff event says custody moved from 0xAA… to 0xBB…. The natural next question is: and where is the proof? The temperature log, the GPS ping, the signed inspection photo, the weighbridge slip.

Today that proof lives in an email thread, an S3 bucket, or a telematics dashboard someone copy-pastes into a dispute ticket. The transition is recorded — the evidence for it is scattered everywhere except next to the transition. And even when a link does make it into the data, a link is not a fact: it proves a pointer existed, not that what it points to is true.

This is Brick #2 — the second experimental proof inside the Bedrij-series hypothesis. It proves a narrow claim: evidence belongs to the transition. Typed evidence references now travel inside the handoff event payload itself — the same append-only record that moves custody — and the replay model replays them with provenance annotations. One primitive, one payload, zero new endpoints.

What is typed evidence in a verifiable supply chain?

Short answer: typed evidence is an optional list of references — {type, hash, issuer, uri?} — attached to a handoff event. type is one of seven values (signature | gps | document | api_response | image | weight | temperature), hash is a 0x-prefixed 32-byte digest of the artifact, issuer is the participant who claims the evidence, and uri is an optional pointer to the artifact itself. A handoff carries 1–32 refs; the replay read model lifts them into hops[].evidence per hop.

The shape matters more than the fields. The hash binds the transition to specific bytes of proof — if the artifact changes, the hash stops matching. The issuer names who asserts the claim, using the same typed-participant shape Brick #1 introduced. And uri stays optional: the ledger records that proof was referenced, not that it was fetched — because fetching is a trust decision each consumer makes separately.

How does evidence travel through a handoff?

Short answer: evidence enters at write time as handoffHop(..., evidenceRefs), is validated by a strict write schema, rides inside the handoff event payload, and is lifted into ShipmentHop.evidence during replay — so GET /api/ledger/shipments/:id returns it per hop, annotated with evidenceAssertedBy (who attached it) and per-ref issuerMatchesParty (whether the issuer is one of the two parties in that hop).

This is the actual replay output from our test pack — hop 0 carries a GPS ref issued by the receiving carrier, hop 1 carries none (addresses shortened for readability):

{
  "shipmentId": "shp-ev-mixed",
  "hops": [
    {
      "hopIndex": 0,
      "status": "paid",
      "receiver": "0xbbbb…bbbb",
      "evidence": [
        {
          "type": "gps",
          "hash": "0xcdcd…cdcd",
          "issuer": { "address": "0xbbbb…bbbb", "type": "org" },
          "uri": "ipfs://QmGps",
          "issuerMatchesParty": true
        }
      ],
      "evidenceAssertedBy": "0xaaaa…aaaa"
    },
    {
      "hopIndex": 1,
      "status": "handed_off",
      "receiver": "0xdddd…dddd"
    }
  ]
}

Three things worth reading in that JSON. issuerMatchesParty: true because the GPS issuer (0xbbbb…) is the counterparty taking custody — a hop party vouching for its own data is weaker evidence than an independent issuer would be, and the flag makes that distinction visible instead of hidden. evidenceAssertedBy is the client (0xaaaa…), not the carrier — the party doing the handoff attached the ref, and the projection records who did it. And hop 1 replays clean with no evidence fields at all: a mixed chain is not an error, evidence is per-transition data.

Diagram: the evidence lifecycle — handoffHop write with evidenceRefs, strict write schema, append-only handoff event, lenient replay, ShipmentHop.evidence plus issuerMatchesParty and evidenceAssertedBy, per-hop evidence in the shipments API
The lifecycle of an evidence reference: asserted at write time by the party doing the handoff, validated by a strict schema, stored in the append-only log, then lifted by a lenient replay into per-hop state with two provenance annotations attached. Nothing in the read model pretends the artifact was fetched or verified — that stays the consumer's call.

Why does a reference need provenance and guardrails?

Short answer: because a reference with no provenance is a forgery primitive. Before the audit, any party could attach an evidence ref claiming it was issued by anyone — a fake "independent inspector" was indistinguishable from a real one. And the strict type/hash/URI rules existed only inside one server wrapper, so every other writer path got the lenient schema.

We ran the deltas through an adversarial AI audit before shipping: 0 Critical, 2 High, 7 Medium findings. The Highs were exactly the trust-boundary bugs: H-1 — no persisted provenance and an inverted advisory (it warned on the honest case and stayed quiet on the forged one); H-2 — strict validation lived in parseHandoffEvidence only, while the registered payload schema and the replay projection stayed lenient, so a forged event replayed into public state. Both are closed: buildEconomicEvent now validates handoff evidence on the write path by default, and replay annotates rather than trusts.

Audit diagram: a forged issuer chip flagged by an amber advisory beacon that warns without blocking; strict write gate versus lenient read gate; canonical-sequence gate rejecting a forged event
Forged-issuer claims hit an advisory, not a block — while the write path enforces the strict schema and replay gates claim-lifting on the canonical event sequence.

What did the audit actually change in the code?

Short answer: a write/read schema split, URI hardening, and three guardrails. buildEconomicEvent(input, {mode}) defaults to strict write validation and offers mode: "replay" for stored events. Evidence URIs are allowlisted to http(s)/ipfs/ar, reject credentials and control characters, and canonicalize to new URL(u).href. The evidence list is pre-checked for cardinality (1–32) before per-item parsing, advisories are aggregated per write, and error echoes are bounded.

The schema split follows the same rule Brick #1 learned: on an append-only log, the read contract can only grow more permissive. The read schema keeps an open evidence type — a future type added by a newer writer must not brick replay for older clients — while the write schema closes on the seven known types. The replay projection adds a belt on top: it safeParses each stored payload and gates claim-lifting on the canonical event sequence, so a forged event with a well-formed payload but a wrong sequence number never lands in the public read model.

Current state: 118 package tests and 35 supply-chain server tests green, zero open Critical/High findings, @agentbadge/econ-events@0.1.3 published to npm. One finding was deliberately accepted rather than fixed: the hop lock is single-process — multi-instance deployments need a distributed lock, documented as a known limitation.

What evidence is NOT — the honest boundary

Short answer: evidence is not truth, not verification, and not enforcement. A hash proves these bytes existed, not that they are accurate. An issuer is a self-declared identity — the same advisory layer Brick #1 established, not a registry lookup. A URI is fetched by nobody: the ledger stores the pointer, consumers decide whether to resolve it. And advisories are log warnings, not verdicts — a third-party issuer flags issuerMatchesParty: false; it does not block the handoff.

Schema card: handoff event payload.evidence fields on the left, replay arrow, hops[].evidence projected read state with issuerMatchesParty and evidenceAssertedBy annotations on the right
The projection adds annotations on read — issuerMatchesParty per ref, evidenceAssertedBy per hop — and never mutates the stored event.

That is deliberate. Brick #2 proves the shape of the claim — "evidence travels with the transition" — not the verification of the artifact. The interesting follow-ups are the missing layers: issuer registries, countersigned evidence (where the named issuer must consent), oracle attestations that verify the artifact itself, and on-chain anchors for the hash. Those are different bricks.

If you're building agent-native supply chains: the schema package is @agentbadge/econ-events, the API is GET /api/ledger/shipments/:id, and the anchor hypothesis this proves a piece of is in The Bedrij Principle, with the participant layer underneath it in Brick #1.

Don't certify. Measure.