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 knows nothing about MCP, JSON-RPC, x402, EIP-712 or EVM. Protocol- and rail-specific representations exist only at adapter boundaries.
| 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 │
├─ authorize + reserve ──► AUTHORIZATION_* │
├─ reserve replayKey ────► PAYMENT_REPLAYED │
├─ settle ───────────────► PAYMENT_SETTLEMENT_FAILED
└─ consume | release | mark the authorization │
│
┌──────────────────────────────────────────────────────┘
├─ call merchant backend ────────────► BACKEND_TIMEOUT / BACKEND_ERROR
├─ store receipt + events ───────────► STORAGE_ERROR
└─ ExecutionOutcome (delivered)verify never moves money; only settle does. The replay reservation sits deliberately between them, so a duplicate authorisation is rejected before any funds move. The authorization step is opt-in per resource and sits in the same place, for the same reason.
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.required → payment.verified → payment.settled
→ backend.called → resource.deliveredA resource requiring authorization adds authorization.verified (or authorization.rejected) between the request and the payment events.
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 tables — receipts, events, payment_attempts — behind a thin repository. payment_attempts.replay_key carries a UNIQUE constraint, which is what makes the replay defence atomic rather than advisory. No secrets and no raw payment proofs are persisted.
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 |
| x402 provider + local/remote facilitator | src/payments/x402 |
| AP2 mandate verification | src/authorization/ap2 |
| SQLite receipts, events, attempts | src/storage/receipts |
| OpenAPI import | src/openapi |
| CLI | src/cli |