Skip to content

Pay any x402 server (the exact rail)

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: { privateKey: process.env.AGENT_KEY! },
schemes: ['onchain-proof', 'exact'], // pay PipRail rails AND standard exact rails
})

With onchain-proof, the client broadcasts the payment itself and proves it. With exact, the buyer signs an EIP-3009 authorization with its own wallet and the server (or a merchant-chosen facilitator) broadcasts it. So the buyer spends roughly zero gas — only the token funds the payment — and PipRail hosts and settles nothing. (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.)

onchain-proof (default)exact (opt-in)
Who broadcastsThe clientThe server / facilitator
Buyer pays gasYes (native coin)No (~0)
Pays which serversPipRail gatesAny standard x402 server
ProofTx ref, verified locallyA signed EIP-3009 authorization

The exact rail is EVM + EIP-3009 only — the canonical USDC and EURC deployments, which expose transferWithAuthorization. The client re-derives each token’s EIP-712 domain on-chain before signing, so a lying or absent server-supplied domain can’t produce a silently-invalid signature.

Works on exactStays on onchain-proof
EVM USDC / EURC (EIP-3009)Any non-EVM family (Solana, TON, …)
An EOA signerUSDT (needs Permit2), native coin, plain ERC-20
A contract / EIP-1271 / EIP-7702 signer

An exact rail is selected only when the 402 names a network your bound EVM chain supports — the client matches each offered rail against its own chain via the driver (it doesn’t gate on a fixed slug list) and settles on that chain. So a USDC/EURC exact rail on the chain your client is bound to is payable; an exact rail naming a different chain (or any non-EVM family) simply isn’t selected and falls back to onchain-proof.

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 EVM driver can actually settle it.

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

Your spend policy and onBeforePay hook gate an exact payment before the wallet signs anything — exactly as they gate an onchain-proof payment.

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'] })

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 enabled
if (!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.

If a 402 offers only an exact rail and the bound family can’t pay it — a non-EVM chain, a non-EIP-3009 token, or a contract / EIP-1271 / EIP-7702 signer — the client throws UnsupportedSchemeError (.code === 'UNSUPPORTED_SCHEME') rather than signing something that can’t settle.

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

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 PaymentTimeoutError carrying the nonce as .ref — the facilitator may have already settled, so verify on-chain and never re-pay.
  • A definitive facilitator rejection (success: false) throws MaxRetriesExceededError — fix the cause, then re-present the same signed authorization, never a fresh one.
  • A 5xx is 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 EIP-3009 authorization nonce (a 0x… 32-byte value, not a tx hash). Recover by checking the token’s authorizationState(from, nonce) and re-presenting the same authorization — never re-sign:

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