Agent transaction audit: a schema you can copy
An append-only schema for agent payment events that satisfies three auditors at once — finance, security, and compliance — without coupling to any specific payment protocol.
The audit schema is the part of agent commerce nobody blogs about and every team ends up needing. Here’s the one we ship — @harpd/audit-schema — designed to satisfy three different auditors at once: finance (was the money spent?), security (was the spend authorized?), and compliance (is the record immutable and reproducible?).
The design constraints
Three constraints shaped every field choice:
- Append-only. No
UPDATEs, noDELETEs. Corrections are new rows that supersede byidempotencyKey. This is the финансы auditor’s first question. - Protocol-agnostic. The same row shape captures an x402 onchain settlement and an MPP card-network hold. The
protocolfield tells the verifier which rail to check. This is the security auditor’s first question. - Reproducible. Given the event row and the chain state at
observedAt, anyone can re-derive the verdict. This is the compliance auditor’s first question.
The schema (SQL)
CREATE TABLE payment_events (
-- identity
id TEXT PRIMARY KEY, -- UUIDv7, sortable
idempotencyKey TEXT NOT NULL, -- (agentId, intentId, endpoint) hash
agentId TEXT NOT NULL,
tenantId TEXT NOT NULL,
-- the payment
endpoint TEXT NOT NULL,
amount INTEGER NOT NULL, -- USDC 6dp minor units
token TEXT NOT NULL, -- CAIP-19
chain TEXT NOT NULL, -- CAIP-2
protocol TEXT NOT NULL, -- 'x402' | 'mpp' | 'ap2'
-- the verdict (filled in by the verifier)
txHash TEXT,
settlementStatus TEXT NOT NULL, -- 'pending' | 'confirmed' | 'failed' | 'diverged'
amountSettled INTEGER, -- what the chain actually moved
verifiedAt TEXT, -- ISO when the verifier ran
verifier TEXT, -- 'onchain' | 'facilitator' | 'multi-source'
-- the lifecycle
hookType TEXT NOT NULL, -- 'before-payment' | 'after-payment' | 'before-settlement' | 'after-settlement'
observedAt TEXT NOT NULL, -- server time, ISO
receivedAt TEXT NOT NULL, -- collector wall-clock
-- audit
rawPayload TEXT, -- the original event JSON, immutable
-- dedupe
UNIQUE (idempotencyKey, hookType, observedAt)
);
CREATE INDEX idx_events_agent ON payment_events (agentId, observedAt);
CREATE INDEX idx_events_endpoint ON payment_events (endpoint, observedAt);
CREATE INDEX idx_events_tenant ON payment_events (tenantId, observedAt);
That’s it. One table, three indexes. Every Harpd feature — dashboards, marketplace scores, alerts, compliance exports — is a SELECT against this table.
Why this shape and not another
amountandamountSettledare separate. They differ when a facilitator reports one thing and the chain settles another. The delta is the divergence detector.rawPayloadis required. If your schema evolves, you can always re-derive the new fields from the old events. Lose the raw payload and you lose the ability to migrate.observedAtandreceivedAtare separate. A 22-minute divergence between the two means the agent’s clock and the collector’s clock disagree by 22 minutes — which is itself a security signal.verifieris recorded. “We verified onchain” means different things depending on which onchain source. A future auditor needs to know whether the row was verified against a public node, your own indexer, or the facilitator’s word.
The deny-list sibling
For audit completeness, denials are first-class — not just “no event was emitted”:
CREATE TABLE payment_denials (
id TEXT PRIMARY KEY,
agentId TEXT NOT NULL,
tenantId TEXT NOT NULL,
endpoint TEXT NOT NULL,
proposedAmount INTEGER NOT NULL,
denyScope TEXT NOT NULL, -- 'perCall' | 'perAgent' | 'perEndpoint' | 'perTool'
denyReason TEXT NOT NULL, -- 'cap_exceeded' | 'token_not_allowed' | 'policy_violation'
policyVersion TEXT NOT NULL, -- which policy was in effect
observedAt TEXT NOT NULL
);
The deny table is what proves a budget was active. Without it, “we have controls” is unverifiable — the auditor can point at a long events table and ask “where would a runaway have shown up?”
How to use it without Harpd
The schema is open-source. If you want to run it yourself:
- Pull
@harpd/audit-schema(the SQL + a tiny TypeScript emitter) - Plug
@harpd/observeinto your x402 client (the events it emits match this schema exactly) - Run the events into your own DB
You lose the dashboard, alerts, and the multi-source verifier — but your audit story is intact. That’s the point. The audit layer is the floor; Harpd is the ceiling.