Core concepts
Architecture
One canonical model, one execution pipeline, and protocol adapters that only translate.
The problem
A merchant already has an HTTP API. AI agents are learning to discover, invoke and pay for capabilities through a growing set of protocols. Implementing each one inside every merchant backend does not scale, and handing the money to a proprietary middleman defeats the point.
The shape of the answer
Three properties are load-bearing:
config.yaml, not by rewriting it.OpenAPI import is an ingress tool, not a second runtime. It terminates at the config boundary: an imported resource is indistinguishable at runtime from one typed by hand.
OpenAPI → importer → resource definitions → canonical model → pipelineCanonical model
The core defines protocol-neutral commerce operations. It uses stable method names such as x402 and mpp for selection, but imports no MCP, JSON-RPC, x402 SDK, EIP-712 or EVM wire types. Protocol- and rail-specific representations exist only at adapter boundaries; a provider's native challenge passes through the core as an opaque value.
| Concept | Type |
|---|---|
| a thing an agent can invoke | CommerceResource |
| what it costs | Pricing |
| how to reach the merchant backend | BackendHandler |
| one inbound call | CanonicalRequest |
| what must be paid, and the opaque provider challenge | PaymentRequirement |
| what happened to a payment | PaymentResult |
| proof of delivery | CommerceReceipt |
| everything worth observing | CommerceEvent |
Adding a protocol or a payment rail is one new adapter rather than a core rewrite, and semantics from one protocol cannot leak into another.
The execution pipeline
Every adapter converges here. Nothing bypasses it.
CanonicalRequest
├─ resolve resource ─────────────────► RESOURCE_NOT_FOUND
├─ validate input ───────────────────► INPUT_INVALID
├─ resolve price
│
├─ free ───────────────────────────────────────────────┐
└─ paid │
├─ createRequirement │
├─ no proof ────► PaymentRequiredOutcome (402) │ fail closed
├─ verify ──────► rejected ► PAYMENT_INVALID │
├─ replayKey missing ────► PAYMENT_INVALID │
├─ authorize + reserve ──► AUTHORIZATION_* │
├─ reserve replayKey ────► PAYMENT_REPLAYED │
│ or STORAGE_ERROR │
└─ upfront flow: settle ─► PAYMENT_SETTLEMENT_FAILED
│
┌──────────────────────────────────────────────────────┘
├─ call merchant backend ────────────► BACKEND_TIMEOUT / BACKEND_ERROR
├─ authorization flow: settle ───────► PAYMENT_SETTLEMENT_FAILED
├─ store receipt + events ───────────► logged on failure, never thrown
└─ ExecutionOutcome (delivered)verify never moves money; only settle does. The replay reservation sits deliberately between them, so a duplicate authorization is rejected before any funds move. The payment flow decides where settle runs. authorization, the x402 default, settles after a successful backend call and withholds the response if settlement fails; upfront, the MPP default, settles before the call. When a challenge lists several x402 offers, the proof selects one before verify, and every later step uses that offer. The authorization step is opt-in per resource and also sits between verify and settle, for the same reason. Its reservation is released only when a failure proves that no money moved.
Correlation
Every flow has one requestId, generated by the protocol adapter and carried through every log line, event, payment attempt and receipt.
resource.requested → payment.verified → backend.called → payment.settled
→ resource.deliveredThe challenge before it is a separate request with its own requestId: resource.requested → payment.required. Under the upfront flow, payment.settled comes before backend.called. A resource requiring authorization adds authorization.verified (or authorization.rejected) between the request and the payment events. The event types are the same whatever the payment or authorization method.
Adapter isolation
An optional adapter that fails to start is marked unhealthy and reported by doctor; it does not stop the process or affect the others. A protocol failure never becomes a payment failure, and vice versa.
Receipts and audit
SQLite, three core tables (receipts, events, payment_attempts) behind a thin repository. payment_attempts.replay_key carries a UNIQUE constraint, which is what makes the replay defense atomic rather than advisory. The pipeline omits raw payment proofs, and row mapping redacts recognized secret fields before anything is written.
payment_attempt_history (schema v3) preserves rejected x402 attempts replaced by retries, including the earlier request and rejection reason.
Schema v4 (2.5.0) adds the transactions journal, resolution metadata and a transaction_id column on receipts, events, payment attempts and attempt history. The migration leaves older rows unchanged, without a transactionId. See Transactions.
Schema v5 (2.6.0) moves AP2 authorization_holds and ACP idempotency_keys into the receipt database. It adds pending_tasks for unpaid A2A x402 tasks to survive restarts, and legacy_imports to track imported files; existing tables stay unchanged. A 2.5 gateway cannot open schema v5. See Storage for upgrades, backups and restores.
Deterministic by construction
LLMs are optional clients of this system, never dependencies of it. Routing, validation, payment verification, receipts and protocol adaptation are deterministic and testable without a model, a public RPC or real money.
Where to look in the code
| Concern | Path |
|---|---|
| canonical model, errors, pipeline | src/core |
| config schema, loader, env substitution | src/config |
| Fastify server, routes, adapter mounting | src/gateway |
| MCP adapter | src/protocols/mcp |
| A2A adapter (experimental) | src/protocols/a2a |
| ACP checkout adapter (experimental) | src/protocols/acp |
| shared bearer check, body reader, URL join | src/protocols/http.ts |
| x402 provider (EVM, Solana, NEAR) + local/remote facilitator | src/payments/x402 |
| MPP provider, settling through an x402 provider | src/payments/mpp |
| AP2 mandate verification | src/authorization/ap2 |
| SQLite receipts, events, attempts, journal, AP2 holds, ACP claims, A2A pending tasks | src/storage/receipts |
| OpenAPI import (config ingress only) | src/openapi |
CLI (init, import openapi, validate, start, doctor, reconcile, resolve, backup, demo, version) | src/cli |
| demo merchant API, buyer agent, dashboard | demo/* |
| MockUSDC and local chain scripts | contracts/, scripts/chain/ |