Skip to content

The exact rail (seller)

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 exact rail and the onchain-proof floor. The buyer signs (0 gas); the facilitator settles + pays. (On Algorand the merchant pays 0 too — see facilitator coverage.)
  • No keyless facilitator for the chaindegrades gracefully to onchain-proof only (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 fallback field explicitly tells the caller to pay the onchain-proof rail instead (the requirePayment Express adapter emits { error: 'settlement_failed', detail, fallback }; with createPaymentGate directly you catch the thrown SettlementError and 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()}` }),
},
}

The gate forwards the request to settleViaFacilitator(), which runs the x402 v2 wire contract against your chosen facilitator:

StepEndpointOutcome
1. VerifyPOST {url}/verifyA cheap early reject (isValid: false → 402) before settling.
2. SettlePOST {url}/settleThe 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.

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

RailWhat the sponsor paysCaps the gate enforces
Algorandthe pooled atomic-group feeMAX_GROUP_FEE = 20 000 µALGO (0.02 ALGO)
Solanabase + compute-budgetMAX_COMPUTE_UNIT_LIMIT = 300 000 units · MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 100 000
Aptosgas (amount × unit price)MAX_GAS_AMOUNT_CAP = 100 000 units · MAX_GAS_UNIT_PRICE_CAP = 2 000 octas/unit
NEARthe relayed gas + the 1-yocto depositMAX_RELAY_GAS = 300 TGas (the honest delegate signs 30 TGas) · attached deposit must equal exactly 1 yoctoNEAR
EVM (EIP-3009 / Permit2)nothing buyer-controllednone 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 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 default
exact: { settle: 'keyless' } // same as `true`
exact: { settle: { facilitator: '' } } // Mode B — a specific facilitator
exact: { settle: 'self', relayer: { key } } // Mode A — your own relayer
FieldTypePurpose
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.
relayera { 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).
Mode A — settle: 'self'Mode B — settle: { facilitator }
Who pays gasYou (relayer)The facilitator
Gasless (no funded key anywhere)No — you fund the relayerYes, with a free facilitator (e.g. PayAI)
Relayer keyRequiredNot needed
Third partyNoneThe facilitator you choose
Bazaar listingNoYes
On a settle failure5xx, authorization stays valid5xx, 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.

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)
},
})

These public exports back the high-level path — reach for them only when hand-rolling an adapter. See the low-level reference.

ExportPurpose
settleViaFacilitatorRun the two-POST verify→settle contract against a facilitator (Mode B core).
FacilitatorConfigA facilitator’s base url + optional authHeaders provider.
FacilitatorPaymentRequirementsThe trusted x402 exact requirements sent to the facilitator.
SettleViaFacilitatorInputThe full input to settleViaFacilitator (config + payload + receipt fields).
readExactDomainRead a token’s true on-chain EIP-712 { name, version } — returns null if not EIP-3009.
eip3009AbiThe minimal seller-side EIP-3009 ABI.