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

> Published: 2026-11-06 | Author: AgentBadge Team | Canonical: https://agentbadge.xyz/blog/supply-chain-typed-evidence

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).
  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](/images/blog/supply-chain-typed-evidence-d1.webp)
  
    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](/images/blog/supply-chain-typed-evidence-2.webp)
  
    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.

  ![Three stacked layers: reference (hash chip), provenance (asserted-by tag), and a dashed future verification layer labelled link is not fact](/images/blog/supply-chain-typed-evidence-3.webp)
  
    Two layers are solid today: a hash that proves these bytes existed, and
    who attached it. Verification is the dashed layer that is not built yet —
    the ledger stores the pointer, not the proof.
  

  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](https://agentbadge.xyz/blog/bedrij-principle), with the
  participant layer underneath it in
  [Brick #1](https://agentbadge.xyz/blog/supply-chain-participant-roles).

*Don't certify. Measure.*

---

## For AI Agents

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