The exact rail (seller)
Introduction
Section titled “Introduction”PipRail gates default to the onchain-proof scheme: the client pays first, then proves it
with a tx ref your gate verifies locally. The ratified x402 exact scheme is the inverse —
the client signs (an EIP-3009
transferWithAuthorization on EVM, a partial-signed TransferChecked transaction on Solana, a
fee-0 asset transfer in a fee-pooled atomic group on Algorand, or a fee-payer sponsored transaction on
Aptos) and someone else broadcasts it. Opting into exact makes your gate payable by any standard
x402 client (and is the only path onto Coinbase’s Bazaar directory), while staying backendless:
PipRail still hosts nothing.
You opt in by passing exact to requirePayment / createPaymentGate.
The gate then dual-advertises: each rail offers an exact entry and the onchain-proof
entry in the same 402, so a standard client picks exact while a PipRail client picks
onchain-proof. Omitting exact leaves the challenge byte-identical to before.
Mode 0 — exact: true (zero-config keyless, start here)
Section titled “Mode 0 — exact: true (zero-config keyless, start here)”The simplest gasless gate is one flag. exact: true (≡ exact: { settle: 'keyless' }) makes the
gate auto-pick a known keyless facilitator for each offered chain — from the seeded, live-verified
KNOWN_FACILITATORS map — so neither the buyer nor you
pays gas, with no relayer key and no facilitator URL to choose:
import { requirePayment } from '@piprail/sdk'
const gate = requirePayment({ chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet', exact: true, // ← auto-picks a keyless facilitator (e.g. PayAI on Base); gasless both sides, zero config})// Live-proven on Base mainnet: payer + merchant both spent ZERO gas; the facilitator broadcast it.It is soft and additive, so it can never brick your gate:
- Has a keyless facilitator (Ethereum, Polygon, Arbitrum, Optimism, Avalanche, Sei, Unichain, Base,
BNB, HyperEVM, Monad, Solana, and Algorand today — 13 chains; more as they’re seeded) → advertises
the gasless
exactrail and theonchain-prooffloor. The buyer signs (0 gas); the facilitator settles + pays. (On Algorand the merchant pays 0 too — see facilitator coverage.) - No keyless facilitator for the chain → degrades gracefully to
onchain-proofonly (the buyer pays gas — the only option left when nobody sponsors) and logs a loud, production-silent warning naming the cause and the remedy. It never throws. - The facilitator is down at settle time → an honest HTTP 502 whose
fallbackfield explicitly tells the caller to pay theonchain-proofrail instead (therequirePaymentExpress adapter emits{ error: 'settlement_failed', detail, fallback }; withcreatePaymentGatedirectly you catch the thrownSettlementErrorand map it yourself). See When the facilitator fails.
Production tip: exact: true is ideal for getting started and for dev. For production, pin a
specific facilitator (Mode B) so an upgrade can’t change which third party settles your payments, or
self-settle (Mode A) to depend on no one. An explicit settle that can’t carry exact throws loudly
(a config error you should fix) — only the soft exact: true degrades.
Mode A — self-settle with your own relayer
Section titled “Mode A — self-settle with your own relayer”You hold a gas-paying relayer key and broadcast the authorization yourself. You pay gas to
receive (the inverse of onchain-proof, where the payer pays gas), and you keep the relayer
funded — but no third party is involved.
import { requirePayment } from '@piprail/sdk'
const gate = requirePayment({ chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet', exact: { settle: 'self', relayer: { key: process.env.RELAYER_KEY } },})// → Express/Connect middleware: drop it in front of a route and the route is paid-only.// The gate dual-advertises `exact` + `onchain-proof` in every 402.The relayer is the gas-paying wallet that broadcasts the settle — distinct from payTo, the
receive address (except on Algorand and Aptos, where it may equal payTo). Pass { key } or bring
your own viem signer with { walletClient }; on Solana pass { key } (a Uint8Array or base58
string) or { signer }; on Algorand pass { key } (a 25-word mnemonic) or { account }; on
Aptos pass { key } (an ed25519-priv-0x… / 0x… hex key) or { account }. It broadcasts
EIP-3009’s transferWithAuthorization (USDC/EURC), the Permit2 proxy’s settle (e.g. BNB), on Solana
co-signs the buyer’s TransferChecked as the fee payer, on Algorand signs the pooled-fee txn and
submits the atomic group, or on Aptos adds the fee-payer signature and submits the sponsored
transaction. Either way the payment binds the recipient (to / witness.to = payTo, the recomputed
recipient ATA on Solana, the verified arcv on Algorand, or the decoded transfer recipient on Aptos),
so a front-runner can only push the same funds to the same payTo — there is no redirect risk.
Mode B — delegate to a facilitator (EVM, Solana and Algorand)
Section titled “Mode B — delegate to a facilitator (EVM, Solana and Algorand)”Instead of running a relayer, delegate verify + settle to a third-party x402 facilitator you choose (Coinbase CDP, PayAI, GoPlausible, or any compatible one). No relayer key, and the facilitator pays gas. Under the hood this is just two HTTP POSTs to the facilitator’s configured URL — PipRail hosts nothing. Works on EVM, Solana, and Algorand (the chains with a seeded keyless facilitator — see coverage).
const gate = requirePayment({ chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet', exact: { settle: { facilitator: 'https://facilitator.payai.network' } },})For a facilitator that needs auth (e.g. Coinbase CDP’s JWT), pass an async authHeaders
provider — its result is merged into every request. Omit it for the free, no-auth facilitators.
exact: { settle: { facilitator: 'https://api.cdp.coinbase.com/platform/v2/x402', authHeaders: async () => ({ Authorization: `Bearer ${await mintCdpJwt()}` }), },}How facilitator settlement works
Section titled “How facilitator settlement works”The gate forwards the request to settleViaFacilitator(), which
runs the x402 v2 wire contract against your chosen facilitator:
| Step | Endpoint | Outcome |
|---|---|---|
| 1. Verify | POST {url}/verify | A cheap early reject (isValid: false → 402) before settling. |
| 2. Settle | POST {url}/settle | The facilitator broadcasts + waits; success: false → 402. |
Both protocol outcomes are HTTP 200 (the boolean flips). A non-200 is a transport or auth
failure — settleViaFacilitator throws a SettlementError, and the gate replies 5xx rather
than a misleading 402. Critically, the paymentRequirements sent to the facilitator are always
rebuilt from the gate’s trusted rail (payTo / amount / asset / network), never the
client’s echo, so a forged payload can’t redirect the settlement.
On the fee-payer rails (Solana, Algorand, Aptos), there’s also a challenge-time read: the gate
fetches the facilitator’s fee-payer / sponsor address from its GET /supported (to advertise it so the
buyer can build the transaction). If that’s unreachable, the gate drops the exact rail for that
chain (serving onchain-proof); if it was the only exact rail, it throws a clear error naming the cause.
Pin it with settle: { facilitator, feePayer } to remove the dependency entirely. The full
three-failure-point breakdown — challenge-time discovery, settle transport/auth (502), and a facilitator
rejection (402) — is in
Gasless payments → When the facilitator fails.
Sponsor protection — the fee-drain guard
Section titled “Sponsor protection — the fee-drain guard”On the fee-payer rails (Solana, Algorand, Aptos, NEAR), whoever sponsors gas — a keyless facilitator in Mode B, or your own relayer in Mode A (the only mode on NEAR) — co-signs and submits a transaction the buyer constructed. That raises a real concern for the party paying the gas: a malicious buyer could set an enormous fee on a sub-cent payment and try to drain the sponsor. Both modes are protected, and the protection is the gate’s, not the facilitator’s — so it applies to your self-settle relayer too.
Before it co-signs, the gate bounds the maximum fee the sponsor will pay, and re-derives every payment
field (payTo, amount, asset, feePayer) from its own trusted rail — never the buyer’s payload.
The caps are generous (≈10× the honest path, so real congestion never trips them) but far below any
meaningful drain; a transaction above a cap is rejected with signature_invalid (and exact: true then
falls back to onchain-proof).
| Rail | What the sponsor pays | Caps the gate enforces |
|---|---|---|
| Algorand | the pooled atomic-group fee | MAX_GROUP_FEE = 20 000 µALGO (0.02 ALGO) |
| Solana | base + compute-budget | MAX_COMPUTE_UNIT_LIMIT = 300 000 units · MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 100 000 |
| Aptos | gas (amount × unit price) | MAX_GAS_AMOUNT_CAP = 100 000 units · MAX_GAS_UNIT_PRICE_CAP = 2 000 octas/unit |
| NEAR | the relayed gas + the 1-yocto deposit | MAX_RELAY_GAS = 300 TGas (the honest delegate signs 30 TGas) · attached deposit must equal exactly 1 yoctoNEAR |
| EVM (EIP-3009 / Permit2) | nothing buyer-controlled | none needed — the buyer signs only an authorization; the relayer/facilitator derives gas at broadcast |
The gate backs the caps with two more structural checks: it accepts only the canonical transfer shape
for each rail, and it rejects any close/rekey (Algorand) or fee-payer-in-an-instruction (Solana) that could
sweep funds. On Solana the canonical shape includes the spec-required SPL-Memo (the rail’s extra.memo,
else a random hex nonce for uniqueness) — the gate tolerates that and any other category-exempt instruction;
the real invariant isn’t a literal instruction count but the single bound TransferChecked (its
recipient, amount, and mint re-derived from the trusted rail), the fee-payer isolation (the fee payer in
no instruction), and the compute-budget caps above. So a relayer or facilitator can only ever pay a
bounded fee to push the signed amount to the trusted payTo. Full detail and the honest-path numbers
are in Gasless payments → Sponsor protection;
the constants live in sdk/src/drivers/{algorand,solana,aptos,near}/exact.ts.
The ExactRailOption
Section titled “The ExactRailOption”The exact: field you pass to requirePayment / createPaymentGate is boolean | ExactRailOption,
exported from @piprail/sdk:
import type { ExactRailOption } from '@piprail/sdk'
exact: true // ≡ { settle: 'keyless' } — auto-pick a keyless facilitator (Mode 0)exact: false // (or omit) — onchain-proof only, byte-identical defaultexact: { settle: 'keyless' } // same as `true`exact: { settle: { facilitator: '…' } } // Mode B — a specific facilitatorexact: { settle: 'self', relayer: { key } } // Mode A — your own relayer| Field | Type | Purpose |
|---|---|---|
settle | 'keyless' | 'self' | { facilitator: string; authHeaders?: () => Promise<Record<string, string>>; feePayer?: string } | Pick the mode: 'keyless' (≡ top-level exact: true) auto-picks a known keyless facilitator from KNOWN_FACILITATORS and degrades gracefully to onchain-proof when none is available; 'self' = your own relayer; { facilitator } = a specific URL you choose. feePayer (Solana only, optional) pins the facilitator’s fee-payer pubkey instead of discovering it from GET /supported. |
relayer | a { key } (or a bring-your-own { walletClient } / { signer }) | Required for settle: 'self' — the gas-paying wallet that broadcasts the settle (EIP-3009 transferWithAuthorization, the Permit2 proxy settle, the Solana fee-payer co-sign, the Algorand pooled-fee txn + group submit, or the Aptos fee-payer signature + submit). Distinct from payTo (must differ on Solana; may equal payTo on Algorand and Aptos). Ignored in facilitator mode. |
method | 'eip3009' | 'permit2' | 'auto' | Which EVM transfer method to advertise. 'auto' (default) uses EIP-3009 when the token supports it, else Permit2 (so BNB’s Binance-Peg USDC “just works”). Pin one to force it. Ignored on Solana (always SVM), Algorand (always the fee-pooled group), and Aptos (always the fee-payer sponsored tx). 'permit2' requires settle: 'self' — a third-party facilitator can’t settle Permit2 (see the Mode B caution above). |
Choosing a mode
Section titled “Choosing a mode”Mode A — settle: 'self' | Mode B — settle: { facilitator } | |
|---|---|---|
| Who pays gas | You (relayer) | The facilitator |
| Gasless (no funded key anywhere) | No — you fund the relayer | Yes, with a free facilitator (e.g. PayAI) |
| Relayer key | Required | Not needed |
| Third party | None | The facilitator you choose |
| Bazaar listing | No | Yes |
| On a settle failure | 5xx, authorization stays valid | 5xx, authorization stays valid |
Mode A is the on-brand default — fully backendless, no third party in the loop. Reach for Mode B when you’d rather not run a relayer, or when you specifically need the Bazaar listing.
What the client signs (and what you verify)
Section titled “What the client signs (and what you verify)”The payer signs off-chain (an EIP-3009 authorization, a Permit2 witness transfer, or — on Solana — a
partial-signed TransferChecked transaction) and never broadcasts — your relayer (Mode A) or the
facilitator (Mode B) does. The buyer side is covered on The exact rail (buyer).
In Mode A, before broadcasting, the gate verifies the inbound payment locally against the trusted
rail: the signature must recover to the authorizer, the recipient must equal payTo, the value must
cover the amount, and it must be unexpired with its nonce unused. On EIP-3009 the EIP-712 domain is
read on-chain from the token, never assumed — canonical USDC’s domain name is "USD Coin" (not
"USDC"), and EURC’s is "Euro Coin" on Ethereum/Avalanche but "EURC" on Base, so only the on-chain
read is authoritative. On Permit2 the same checks apply (witness.to = payTo, permitted.amount
≥ the price, the Permit2 nonce unused, spender = the canonical x402ExactPermit2Proxy). On Solana
(SVM) the gate re-derives the recipient ATA from payTo, requires the TransferChecked mint + amount
to match, enforces the fee-payer safety rules (the fee payer in no instruction, never a program, never
drained), checks the buyer’s signature via a sigVerify simulation, then co-signs as fee payer and
broadcasts.
Replay protection and onPaid
Section titled “Replay protection and onPaid”Whichever mode you use, the EIP-3009 authorization nonce is replay-claimed in the gate’s
used-proof set (the on-chain authorizationState is a second, canonical guard). Multi-instance
deploys share state through the same isUsed / markUsed
hooks as onchain-proof. A settled exact payment fires the same
onPaid callback, with a receipt whose
scheme is 'exact' and whose transaction is the settle tx hash.
const gate = requirePayment({ chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet', exact: { settle: 'self', relayer: { key: process.env.RELAYER_KEY } }, onPaid: (receipt) => { console.log(receipt.scheme, receipt.transaction) // → 'exact' '0x9f…' (the settle tx your relayer broadcast) },})Low-level building blocks
Section titled “Low-level building blocks”These public exports back the high-level path — reach for them only when hand-rolling an adapter. See the low-level reference.
| Export | Purpose |
|---|---|
settleViaFacilitator | Run the two-POST verify→settle contract against a facilitator (Mode B core). |
FacilitatorConfig | A facilitator’s base url + optional authHeaders provider. |
FacilitatorPaymentRequirements | The trusted x402 exact requirements sent to the facilitator. |
SettleViaFacilitatorInput | The full input to settleViaFacilitator (config + payload + receipt fields). |
readExactDomain | Read a token’s true on-chain EIP-712 { name, version } — returns null if not EIP-3009. |
eip3009Abi | The minimal seller-side EIP-3009 ABI. |