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.
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.
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.
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.
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.