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:

Self-hostedThe gateway runs in the merchant's infrastructure. There is no central service, and none is planned.
Non-custodialIt orchestrates a payment protocol; it never holds funds or keys.
ConfigurationYou expose an existing endpoint by describing it in 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.

text
OpenAPI → importer → resource definitions → canonical model → pipeline

Canonical model

The core knows nothing about MCP, JSON-RPC, x402, EIP-712 or EVM. Protocol- and rail-specific representations exist only at adapter boundaries.

ConceptType
a thing an agent can invokeCommerceResource
what it costsPricing
how to reach the merchant backendBackendHandler
one inbound callCanonicalRequest
what must be paid, and the opaque provider challengePaymentRequirement
what happened to a paymentPaymentResult
proof of deliveryCommerceReceipt
everything worth observingCommerceEvent

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.

text
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.

text
resource.requested → payment.required → payment.verified → payment.settled
                   → backend.called → resource.delivered

A 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

ConcernPath
canonical model, errors, pipelinesrc/core
config schema, loader, env substitutionsrc/config
Fastify server, routes, adapter mountingsrc/gateway
MCP adaptersrc/protocols/mcp
x402 provider + local/remote facilitatorsrc/payments/x402
AP2 mandate verificationsrc/authorization/ap2
SQLite receipts, events, attemptssrc/storage/receipts
OpenAPI importsrc/openapi
CLIsrc/cli