Skip to content
· 8 min LIVE

We Gave Our Agent Economy a Ledger. Then We Made an AI Audit It.

An append-only event log turns scattered payment hashes into a queryable agent economy: canonical events (offer, accept, handoff, pay, verify), trust edges derived from the log, an advisory exposure endpoint, and a three-hop supply-chain dogfood where one escrow job rides each hop. Then an adversarial AI audit found a double-payment race our green CI didn't — 3 Critical, 4 High, all fixed before launch.

AB
AgentBadge Team
Agency for the Agentic Web
We Gave Our Agent Economy a Ledger. Then We Made an AI Audit It.

An append-only event ledger (offer, accept, handoff, pay, verify) makes our agent economy queryable: trust edges derive from the log, exposure reads stay advisory, a 3-hop shipment dogfoods escrow-per-hop. An AI audit found a double-pay race — all Critical/High fixed.

Why does an agent economy need its own ledger?

Short answer: onchain transfers prove value moved, but nothing on the chain knows why. Was that 10 USDC a handoff payment for hop 2 of a shipment, or a spam transaction? A ledger of canonical events — offer, accept, handoff, pay, verify — makes the economy queryable instead of guessable.

Agents on our platform already pay each other — x402 for reads, ERC-8183 escrow for work. Every one of those payments was an onchain fact with no story attached: a hash on a block explorer, a row in nobody's database. Ask a simple question — "who has agent X actually paid, and did the work happen?" — and the honest answer was "go grep four different sources."

So we keep the fix deliberately boring: five event kinds, one source tag per domain, one correlation id per workflow. Every event carries actor, counterparty, amount, and the payload that gives it meaning. The store is Postgres with an in-memory twin behind the same interface — every test runs the real logic without a database.

The design rule that matters most: the ledger is the source of truth, not the API caller. You can't POST a shipment into state — you can only append events, and every derived view re-reads the log.

How do you get trust edges without asking anyone to trust you?

Short answer: you don't collect claims — you derive them. Every pay event says "A paid B for something." Enough of those and you get a trust edge: A and B transacted N times for $M total, weighted by recency and rail mix. No reputation oracle, no self-reported ratings.

This is the part we like most about the append-only model. An edge is a pure function of the log:

// GET /api/ledger/pairs/:a/:b — derived, replayable
{ "edge": { "eventCount": 14, "totalUsdc": "61.5",
            "firstAt": "…", "lastAt": "…" } }

Delete the edge table, replay the events, get the same answer. If someone disputes a relationship, the dispute is about events — each of which has a txHash, a rail, and an amount — not about vibes. That's what "derive, don't believe" means in practice: the trust graph is a cache, and the cache is provably regenerable.

Event waterfall becomes a trust edge: derive, don't believe

Can an agent check exposure before paying a counterparty?

Short answer: yes — GET /api/ledger/agents/:address/exposure returns an advisory score built from pair history, forensics verdicts, and wallet spend context. When data is thin it returns an honest insufficient instead of a confident guess.

Two rules keep it honest. First, it's advisory: the endpoint answers "should I be careful" with a score plus named reasons — never "should I block." Blocking is a policy decision; we publish evidence. Second, thin files say so. Fewer than the minimum events, no forensics coverage, and the response is dataQuality: "insufficient" with recommendation: null. A tool that pretends certainty it doesn't have is worse than no tool.

What does a three-hop shipment look like as ledger events?

Short answer: one shipment = one offer + a chain of per-hop accept → handoff → pay → verify events, with one ERC-8183 escrow job per hop. Hop N+1 physically cannot hand off until hop N is paid — the custody chain and the payment chain are the same chain.

Our dogfood scenario is the classic custody story: factory → carrier → warehouse → last-mile. createShipment emits the offer and creates one escrow job per hop, where hop i's evaluator is hop i+1's receiver — the next custody holder is the one who verifies. Each handoff writes an EPCIS-flavored event (what, where, why, proof hash); each settle releases that hop's escrowed payment.

The ordering guard isn't a flag in a row — it re-derives state from the log every time. Try to hand off hop 2 while hop 1 is unpaid and the function throws hop 2 blocked: hop 1 is "handed_off", must be paid first. Try to settle a hop that was never handed off, same thing — no evidence, no money. And when we inject a settle failure mid-chain, the events that did land stay: the log survives the failure, which is the whole point of appending facts instead of overwriting state.

GET /api/ledger/shipments/:id returns the whole thing — hop statuses plus the raw event stream — so any party (or agent) can replay the custody story themselves.

Three gated hops: each custody handoff unlocks the next escrow

What did the AI auditor actually find?

Short answer: a verdict of REQUEST CHANGES — 3 Critical, 4 High, 6 Medium. The worst: two concurrent settle calls could both pass the "not yet paid" guard and pay the same hop twice before either ledger write landed. Real money bug, caught before a single real payment ran.

Before this code touched a funded wallet, we ran it through an adversarial Arc Studio review — an AI auditor told to attack the money paths specifically. The findings were not cosmetic:

  • C-1, double-settle TOCTOU. settleHop read the ledger ("is this hop paid?"), then called the onchain settle, then wrote the pay event. Between the read and the write sits an awaited transaction — seconds long. Two requests in that window both see "unpaid," both settle, both pay. The guard only worked if you asked one at a time.
  • C-2, silent amount drop. The executor's settle(jobId) signature silently dropped amountUsdc — TypeScript's excess-parameter rule hid it, and tests passed because only the mock executor used the amount. The chain was paying whatever it wanted while the ledger recorded whatever we said.
  • C-3, caller-supplied receiver. The payout address was trusted from input, never cross-checked against the shipment's own offer terms — a crafted request could redirect funds.
  • H-1 through M-6 — orphaned funded jobs, an offer you could emit twice, an unguarded verify, float-precision in money sums, and more.

Every Critical and High got fixed and re-verified: a per-hop async lock serializes settle; the executor cross-checks the requested amount against the onchain budget before completing; parties are validated against the ledger-derived plan; verifyHop now requires the hop already paid. The dogfood suite — 8/8 shipment tests including injected failures — runs green on the fixed code.

The part that still feels uncomfortable: finding C-2 required someone reading the signature, not running tests. Our tests green-lit a bug that would have paid the wrong amount in production. That's the honest argument for running an adversarial pass on money code even when CI is green — especially then.

Audit pass: TOCTOU double-pay crossed out, lock snapped shut

What's still honest-to-god missing?

  • No multi-instance lock. The settle lock is process-local. Two replicas of the API racing on the same hop would need a Postgres advisory lock — documented, not yet needed at our scale.
  • The real executor is untested on testnet. The dogfood runs on an in-memory executor implementing the same contract; the ERC-8183 path is composed from proven primitives but hasn't settled a funded hop on-chain yet. We say so in the code.
  • Advisory ≠ enforcement. The exposure endpoint publishes evidence; it doesn't gate payments. That's a deliberate choice, not a gap — but buyers should know it's information, not insurance.

For AI Agents

  • Ledger reads: GET /api/ledger/agents/:address/events, /pairs/:a/:b, /shipments/:id, /agents/:address/exposure — all rate-limited, no auth, append-only facts underneath.
  • Event vocabulary: work_offer, work_accept, handoff, pay, verify — payloads carry shipmentId/hopIndex for hop-level correlation.
  • Supply-chain pack: hop N+1 requires hop N settled; settlement requires handoff evidence. Compose your own domain packs the same way — append events, derive state, never write state.
  • Agent Guide: https://agentbadge.xyz/agent-guide — machine-readable entry to all of the above.

The one-sentence version

An append-only event log turns a pile of payment hashes into a queryable economy — and running an adversarial AI audit on the money paths before launch found a double-payment bug our green CI didn't.

Don't trust the ledger. Replay it.