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:

  1. Append-only. No UPDATEs, no DELETEs. Corrections are new rows that supersede by idempotencyKey. This is the финансы auditor’s first question.
  2. Protocol-agnostic. The same row shape captures an x402 onchain settlement and an MPP card-network hold. The protocol field tells the verifier which rail to check. This is the security auditor’s first question.
  3. 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

  • amount and amountSettled are separate. They differ when a facilitator reports one thing and the chain settles another. The delta is the divergence detector.
  • rawPayload is 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.
  • observedAt and receivedAt are 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.
  • verifier is 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/observe into 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.

Next