How InFlow's three @inflowpayai/x402* packages compose with the foundation V2 middleware and buyer transport to
deliver an x402 integration.
InFlow does not ship seller middleware. The foundation already ships adapters for Express, Fastify, Hono, and Next.js; it owns the request loop, payment response cache controls, paywall, settlement hooks, and multi-facilitator resolution via declaration order. InFlow plugs into that — the seller-side value-add is three factories and two helpers:
| InFlow surface | Returns | Drops into |
|---|---|---|
createInflowFacilitator |
foundation FacilitatorClient |
a foundation adapter's facilitatorClients argument |
createUnauthenticatedInflowFacilitator |
foundation FacilitatorClient |
same — for facilitator-only deployments |
createInflowSellerClient |
InflowSellerClient |
drives inflowAccepts |
inflowAccepts(client, options) |
foundation PaymentOption[] |
a route's accepts field in RoutesConfig |
inflowSchemeRegistrations(client) |
Promise<SchemeRegistration[]> |
a foundation adapter's schemes argument — the foundation refuses to boot without these registrations |
The buyer side ships InflowClient, a subclass of the foundation's x402Client. The buyer composes by passing the
InflowClient instance to the foundation's x402HTTPClient transport and to any registerExactEvmScheme /
registerExactSvmScheme helpers — same client, one routing override, no parallel wrapper packages.
@inflowpayai/x402 (core: types, http client, constants)
/ \
/ \
@inflowpayai/x402-seller @inflowpayai/x402-buyer
(InflowClient extends x402Client)
The seller package has no framework adapter packages — sellers depend on @x402/express, @x402/hono, @x402/fastify,
or @x402/next directly. The buyer package likewise has no transport adapter — buyers use @x402/core's
x402HTTPClient with their preferred HTTP client (fetch, axios, etc.).
buyer foundation middleware InFlow facilitator InFlow server
│ │ │ │
│ GET /api/widgets │ │ │
│ ────────────────────────▶ │ │ │
│ │ match route? │ │
│ │ (init) check hasRegisteredScheme │ │
│ │ for every advertised scheme │ │
│ │ (inflowSchemeRegistrations │ │
│ │ covers balance and friends); │ │
│ │ then get supported per │ │
│ │ facilitator client; first │ │
│ │ claimer of (scheme, network) │ │
│ │ wins routing │ │
│ │ ─────── getSupported() ────────▶ │ GET /v1/x402/supported │
│ │ │ ───────────────────────▶ │
│ │ ◀───── SupportedResponse ─────── │ ◀─────────────────────── │
│ │ │ │
│ │ assemble PaymentRequired from │ │
│ │ route's accepts[] (built │ │
│ │ from inflowAccepts) │ │
│ 402 + PAYMENT-REQUIRED │ │ │
│ ◀──────────────────────── │ │ │
│ │ │ │
│ GET /api/widgets + │ │ │
│ PAYMENT-SIGNATURE │ │ │
│ ────────────────────────▶ │ │ │
│ │ decode payload │ │
│ │ route to facilitator by │ │
│ │ (scheme, network) │ │
│ │ ─────── verify ────────────────▶ │ POST /v1/x402/verify │
│ │ │ ───────────────────────▶ │
│ │ ◀──── { isValid: true } ──────── │ ◀─────────────────────── │
│ │ │ │
│ │ next() → protected handler │ │
│ │ res.end intercepted │ │
│ │ ─────── settle ────────────────▶ │ POST /v1/x402/settle │
│ │ │ ───────────────────────▶ │
│ │ ◀──── SettleResponse ─────────── │ ◀─────────────────────── │
│ 200 + PAYMENT-RESPONSE │ │ │
│ ◀──────────────────────── │ │ │
The seller fetches /v1/x402/config once at startup (via createInflowSellerClient), expands it into PaymentOption[]
via inflowAccepts, and hands the result to the foundation middleware in each route's accepts field. The foundation
middleware never calls /v1/x402/config itself — it consumes PaymentOption[] shapes that already have payTo,
asset, and atomic amount resolved.
caller x402HTTPClient InflowClient InFlow server
│ │ │ │
│ fetch(url) │ │ │
│ ─────────────────────────────────────────────────────────────────────────────────────▶ │ (seller)
│ ◀─ 402 + PAYMENT-REQUIRED ──────────────────────────────────────────────────────────────│
│ │ │ │
│ getPaymentRequiredResponse(headers) │ │
│ ─────────────────▶ │ │ │
│ │ createPaymentPayload(req) │ │
│ │ ──────────────────────────▶│ │
│ │ │ pickInflowMatch → InFlow branch │
│ │ │ POST /v1/transactions/x402 │
│ │ │ ────────────────────────────────────▶ │
│ │ │ ◀── approvalId, ... ───────────────── │
│ │ │ poll GET /v1/transactions/{id}/x402 │
│ │ │ ────────────────────────────────────▶ │
│ │ │ ◀── { status, paymentPayload, ... } ──│
│ │ ◀── PaymentPayload ────── │ │
│ encodePaymentSignatureHeader(payload) │ │
│ ◀───────────────── │ │ │
│ │ │ │
│ retry fetch(url) + PAYMENT-SIGNATURE │
│ ─────────────────────────────────────────────────────────────────────────────────────▶ │ (seller)
│ ◀── 200 + PAYMENT-RESPONSE ─────────────────────────────────────────────────────────────│
│ processResponse(paid) → x402PaymentResult │
│ ─────────────────▶ │ │ │
│ ◀── { kind: 'success', body, settleResponse } ──│ │
The InflowClient.createPaymentPayload call is two-phase under the hood when it routes to the InFlow branch: a
synchronous POST /v1/transactions/x402 creates the buyer's Approval, then a polling loop on
GET /v1/transactions/{id}/x402 waits for the server to sign. The polling cadence is a fixed 5 s; the default total
budget is 15 minutes. The foundation transport (x402HTTPClient.encodePaymentSignatureHeader) re-encodes the parsed
payload via JSON.stringify + base64, and InFlow decodes the standard base64 + JSON form, so the round trip is
wire-equivalent.
When no accepts[] entry matches the InFlow buyer capability cache, the override delegates to
super.createPaymentPayload, which uses the foundation's selector to route to whatever scheme the caller registered on
the same InflowClient instance via registerExactEvmScheme / registerExactSvmScheme / etc.
The foundation middleware resolves overlapping (scheme, network) claims by declaration order in the
facilitatorClients array: at startup it walks the array, takes each facilitator's getSupported(), and assigns each
(scheme, network) pair to the first facilitator that claims it. First claimer wins; subsequent claimers are silently
ignored. Sellers control resolution by ordering the array:
paymentMiddlewareFromConfig({/* routes */}, [
inflow, // wins on (balance, inflow) and any (exact, *)
cdp, // wins only on entries inflow doesn't claim
partnerNetwork,
]);The buyer side uses a different but parallel rule: InflowClient.createPaymentPayload checks the InFlow buyer
capability cache first (in prefer-scheme order), then falls back to super.createPaymentPayload for any requirement
InFlow can't sign. The fallback uses the foundation's own selector against the schemes registered on the client. InFlow
always wins when its cache matches; foundation schemes only run when no InFlow (scheme, network) pair fits.
Given an InflowSellerClient and a PriceSpec, inflowAccepts produces a foundation PaymentOption[]:
- On-chain entries: for each
walletin the seller's config, match everyassetwithasset.blockchain === wallet.blockchainand a compatible currency. Emit onePaymentOptionper(wallet, asset)pair usingasset.assetTransferMethodverbatim — the SDK does not fan out an implicit EIP-3009/Permit2 pair. Sellers that want both schemes publish both assets in their/v1/x402/config. For Permit2 entries,extra.permit2Proxyis set fromasset.permit2Proxy. - Non-blockchain entries: for each
paymentMethod(balance, futureinstrument), emit onePaymentOptionusing the method's ownpayToand decimals. - Filter:
options.schemesandoptions.networksare combined as logical AND. Omit either for "any."
Ordering: on-chain entries by wallet declaration order, then payment methods in declaration order.
Extension declarations are not produced by inflowAccepts — per-route declarations live on RouteConfig.extensions,
and facilitator-wide declarations come from each FacilitatorClient.getSupported().extensions and are merged by the
middleware.
The foundation middleware checks hasRegisteredScheme(scheme, network) before it consults any
FacilitatorClient.getSupported(). A facilitator that advertises support for a scheme the middleware doesn't know how
to register cannot be used: the middleware refuses to boot. inflowSchemeRegistrations() returns the passthrough
SchemeRegistration[] for balance (and any future InFlow-managed schemes) and is meant to be passed in the adapter's
schemes argument alongside any framework-native registrations (registerExactEvmScheme / registerExactSvmScheme).
For Express and Hono:
paymentMiddlewareFromConfig(
routes,
[inflowFacilitator /* others */],
[...(await inflowSchemeRegistrations(client)), registerExactEvmScheme(), registerExactSvmScheme()],
);The registrations are passthrough — they don't sign or settle anything themselves; the InFlow facilitator handles both.
They satisfy the middleware's scheme-knowledge check at boot and declare the foundation's lifecycle contract. For each
(scheme, network), the helper aggregates exactly the assetTransferMethod values emitted by config; methods that omit
that field use the foundation's SDK-only default sentinel. Every resulting entry supports only the authorization
flow, preserving verify-before-handler and settle-after-handler behavior. The helper never enables upfront or escrow
implicitly.
InflowClient.prepareInflowPayment() issues POST /v1/transactions/x402 synchronously, which creates the server-side
Approval before returning. If the caller aborts after prepareInflowPayment() resolves but before awaitPayload()
returns, the Approval would otherwise sit pending until it expires server-side. Three things together keep this clean:
InflowClient.createPaymentPayload()internally calls the InFlow signer's one-shot path, which wrapsprepare→awaitPayloadwith an auto-cancel-on-error fire-and-forget POST. A failed sign cleans itself up.PreparedPayment.cancel()is fire-and-forget — it never rejects. Safe to call unconditionally in afinally.- Server-side expiry bounds the worst case even when the cancel POST fails to land.
- protocol-mapping.md for wire-shape details and network identifier rules.
- extensions.md for the extension handler contract.