Pay any x402 server (the exact rail)
Introduction
Section titled “Introduction”By default a PipRailClient pays only PipRail’s native
onchain-proof rail — the backendless scheme where the client pays first and proves it with a
tx ref. That covers every PipRail gate, but most of the public x402 web (the dominant
exact-on-Base flow) speaks the ratified exact scheme instead. Opt into it and the same
client can pay any standard x402 server.
import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'base', wallet: { key: process.env.AGENT_KEY! }, schemes: ['onchain-proof', 'exact'], // pay PipRail rails AND standard exact rails})How the exact rail differs
Section titled “How the exact rail differs”With onchain-proof, the client broadcasts the payment itself and proves it. With exact, the
buyer signs with its own wallet and someone else broadcasts it — the merchant’s relayer, or a
merchant-chosen facilitator (keyless on EVM EIP-3009, Solana, and Algorand). So the buyer spends
roughly zero gas — only
the token funds the payment — and PipRail hosts and settles nothing. The buyer is gasless either way:
how the merchant settles (its own relayer vs a facilitator) is the merchant’s call and invisible to the
buyer. When the merchant points settlement at a free facilitator like PayAI, no one runs a
gas-funded key at all — settlement is fully gasless end to end (see
Gasless payments).
onchain-proof (default) | exact (opt-in) | |
|---|---|---|
| Who broadcasts | The client | The server / facilitator |
| Buyer pays gas | Yes (native coin) | No (~0) |
| Pays which servers | PipRail gates | Any standard x402 server |
| Proof | Tx ref, verified locally | A signed EIP-3009 authorization, a Permit2 witness, a partial-signed Solana transaction, an Algorand fee-pooled ASA group, an Aptos sponsored (fee-payer) transaction, or a NEAR NEP-366 SignedDelegateAction |
What exact can settle
Section titled “What exact can settle”The exact rail works on EVM, Solana, Algorand, Aptos, and NEAR, via one of six on-chain methods. The
402’s rail names which one (extra.assetTransferMethod), and the client picks the matching signer
automatically:
eip3009(EVM) — canonical USDC/EURC and other tokens exposingtransferWithAuthorization. The client re-derives the token’s EIP-712 domain on-chain before signing, so a lying or absent server-supplied domain can’t produce a silently-invalid signature. Fully gasless for the buyer.permit2(EVM) — any ERC-20 without EIP-3009, most notably Binance-Peg USDC/USDT on BNB Chain (no native Circle USDC exists on BNB). The client signs a Permit2PermitWitnessTransferFromwhosespenderis the canonical x402ExactPermit2Proxy and whosewitness.tobinds the recipient (so a relayer can’t redirect funds). Gasless per-payment too — after a one-timeapprove(Permit2)the SDK does lazily the first time you pay that token.svm(Solana) — any SPL token (USDC, USDT, …). The client builds the SPLTransferCheckedwith the merchant as the transaction fee payer, adds a spec-required SPL-Memo instruction (the rail’sextra.memo, else a random hex nonce) for transaction uniqueness, signs only its own slot, and sends the partially-signed transaction; the gate co-signs as fee payer and broadcasts. No EIP-3009 equivalent, no proxy, no approval — gasless for the buyer regardless of token. See Gasless payments.algorand(Algorand) — any ASA (USDCa, …). The client signs an ASA transfer at fee 0, atomically grouped with the sponsor’s fee-poolingpay; the sponsor signs that fee txn and submits the group. No token feature required — gasless for the buyer regardless of token.aptos(Aptos) — any Fungible Asset (USDC, USD₮, …). The client signs a fee-payer (sponsored, AIP-39)primary_fungible_store::transfer(sender slot only); the sponsor adds the fee-payer signature and submits. No token feature required — gasless for the buyer regardless of token.near(NEAR) — any NEP-141 token (USDC, USDT). The client signs a NEP-366SignedDelegateActionwith its full-access key authorizing exactly oneft_transfertopayTo(the exactamount,deposit: 1yoctoNEAR, fixed 30 TGas); the merchant’s relayer wraps it in its own outer transaction, prepays the gas and the yocto, and submits. The buyer holds zero NEAR — gasless regardless of token. Self-settle only today (no third-party NEAR x402 facilitator settles yet — see Gasless payments).
Works on exact | Stays on onchain-proof |
|---|---|
| EVM EIP-3009 (USDC / EURC; FDUSD, USD1 & U on BNB) | The other non-EVM families (TON, Tron, Sui, Stellar, XRPL) |
| EVM Permit2 — any ERC-20 (e.g. Binance-Peg USDC on BNB) | The chain’s native coin (incl. SOL, ALGO, APT, NEAR) |
| Solana SVM — any SPL token (USDC / USDT) | A contract / EIP-1271 / EIP-7702 signer (EVM) |
| Algorand ASA (USDCa) · Aptos FA (USDC / USD₮) · NEAR NEP-141 (USDC / USDT, via NEP-366 meta-tx) |
An exact rail is selected only when the 402 names a network your bound chain supports — the
client matches each offered rail against its own chain via the driver (matching the network whether
it’s a CAIP-2 id or a chain slug — see Interoperability below)
and settles on that chain. So an EIP-3009/Permit2 rail on your bound EVM chain, or an SVM rail on
a Solana-bound client, is payable; an exact rail naming a different chain (or a family without an
exact scheme) simply isn’t selected and falls back to onchain-proof.
Interoperability: any network label
Section titled “Interoperability: any network label”The exact rail is the standard x402 scheme, so a PipRail client interoperates with the wider x402
ecosystem out of the box — any server or facilitator that speaks exact, however it labels the
network. Before matching a rail to your bound chain the client normalizes the rail’s network, so a
402 that names the chain as a CAIP-2 id (eip155:8453) or a chain slug (base, bsc,
polygon) is matched and paid identically. You don’t have to know, or configure, which form a given
facilitator emits — both resolve to the same chain.
This matters because facilitators in the wild are inconsistent: the same endpoint may advertise a rail
as eip155:56 in one place and bsc (or 56) in another. PipRail pays all of them. A label that
resolves to a different chain than the one you’re bound to — or an unrecognized one — is simply not
selected, never mis-paid; the trusted EIP-712 domain (which fixes the chain id at signing time) is the
final guard regardless of the label.
When you enable both schemes, the client gathers onchain-proof rails first, so on a dual-rail
402 the default selection is unchanged. An exact rail is only ever picked when the bound
driver can actually settle it (EVM EIP-3009/Permit2, Solana SVM, the Algorand ASA rail, the Aptos FA rail, or the NEAR NEP-366 meta-tx).
To make the client prefer the gasless exact rail when a gate offers both, enable
autoRoute (new PipRailClient({ …, autoRoute: true }), or
per call fetch(url, { autoRoute: true })): it pays the cheapest settleable rail, and since the
buyer-gasless exact rail estimates at ~0 gas, it wins automatically. Without autoRoute the dual-rail
default stays onchain-proof; a foreign exact-only server is paid over exact either way.
Paying
Section titled “Paying”Once a scheme is enabled, paying is the same call as ever — fetch/get/post
handle the 402 transparently and pick the right path per rail:
const res = await client.get('https://api.example.com/report')const data = await res.json()// → the gated JSON, paid for via exact (or onchain-proof) transparentlyYour spend policy and onBeforePay hook gate an exact
payment before the wallet signs anything — exactly as they gate an onchain-proof payment.
Enabling it per call
Section titled “Enabling it per call”You can leave the constructor on the default and flip schemes for a single request, overriding
the constructor’s schemes for that call:
const url = 'https://api.example.com/report'await client.fetch(url, { schemes: ['exact'] })Read-only planning sees exact too
Section titled “Read-only planning sees exact too”planPayment() and quote() honour
the enabled schemes. On an exact rail, only the token balance gates payability (the buyer
spends no gas), so an INSUFFICIENT_GAS blocker never applies and gas-basis warnings are
suppressed.
const url = 'https://api.example.com/report'const plan = await client.planPayment(url) // analyses exact rails when enabledif (!plan) { await client.fetch(url) // not gated — fetch it for free} else if (plan.payable) { await client.fetch(url, { autoRoute: true })} else { console.log(plan.fundingHint) // one-line, human-readable: what to top up}planPayment() returns null when the URL isn’t payment-gated, so null-guard it before reading
payable.
When exact can’t settle
Section titled “When exact can’t settle”If a 402 offers only an exact rail and the bound family can’t pay it — a family without an
exact scheme (TON, Tron, Sui, Stellar, XRPL), the chain’s native coin
(incl. SOL, ALGO, APT, NEAR), or a contract / EIP-1271 / EIP-7702 signer — the client throws
UnsupportedSchemeError (.code === 'UNSUPPORTED_SCHEME') rather
than signing something that can’t settle. (A non-EIP-3009 ERC-20 is not in this list — it pays
via Permit2; nor is an SPL token on Solana — it pays via SVM.)
import { PipRailClient, UnsupportedSchemeError } from '@piprail/sdk'
const url = 'https://api.example.com/report'
try { await client.fetch(url, { schemes: ['exact'] })} catch (err) { if (err instanceof UnsupportedSchemeError) { // this chain/asset/signer can't pay the exact rail — fall back to onchain-proof console.error(err.message) } else { throw err }}Failure modes worth knowing
Section titled “Failure modes worth knowing”The exact pay path is deliberately more conservative than the onchain-proof retry loop: the
buyer signs once and the same header is re-presented on every retry — it never re-signs a
fresh nonce.
- A transport error or timeout after the authorization is submitted throws
PaymentTimeoutErrorcarrying the nonce as.ref— the facilitator may have already settled, so verify on-chain and never re-pay. - A definitive facilitator rejection (
success: false) throwsMaxRetriesExceededError— fix the cause, then re-present the same signed authorization, never a fresh one. - A
5xxis returned as-is: a server-side settle failure leaves your authorization valid and its nonce unused, so nothing is recorded as spent.
On an exact rail, the .ref carried by PaymentTimeoutError / MaxRetriesExceededError is the
authorization nonce — the EIP-3009 nonce (a 0x… 32-byte value) or, on the Permit2 method, the
Permit2 nonce (a uint256). It is not a tx hash. Recover by checking the nonce’s on-chain state
(EIP-3009 authorizationState(from, nonce), or the Permit2 nonce bitmap) and re-presenting the
same authorization — never re-sign:
On the non-EVM exact rails the .ref is likewise the family’s single-use marker, recovered
differently: Solana — the buyer’s transaction signature (a duplicate signature is the chain’s
replay guard; plus the SPL-Memo nonce when present); Algorand — the atomic group / transaction
id; Aptos — the sender’s account sequence number; NEAR — the access-key nonce (carried as
accountId:nonce). The discipline is identical: verify the marker on-chain, re-present the same
signed payload, never re-sign and never re-pay.
import { PaymentTimeoutError, MaxRetriesExceededError } from '@piprail/sdk'
const url = 'https://api.example.com/report'
try { await client.fetch(url, { schemes: ['exact'] })} catch (err) { if (err instanceof PaymentTimeoutError || err instanceof MaxRetriesExceededError) { // .ref exists ONLY on these two classes — the EIP-3009 nonce on the exact rail console.log('recover with this authorization nonce, do NOT re-pay:', err.ref) } else { throw err }}