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 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 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: { privateKey: 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 transfer — distinct from payTo, the receive address. Pass an EVM { privateKey }, or bring your own viem signer with { walletClient }. Self-settle uses transferWithAuthorization (not receiveWithAuthorization), and the signature binds to = payTo, so a front-runner can only push the same funds to the same payTo and waste their own gas — there is no redirect risk.

Instead of running a relayer, delegate verify + settle to a third-party x402 facilitator you choose (Coinbase CDP, x402.org, PayAI, 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.

const gate = requirePayment({
chain: 'base', token: 'USDC', amount: '0.10', payTo: '0xYourWallet',
exact: { settle: { facilitator: 'https://x402.org/facilitator' } },
})

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.

The exact: object you pass to requirePayment / createPaymentGate is an ExactRailOption, exported from @piprail/sdk:

import type { ExactRailOption } from '@piprail/sdk'
FieldTypePurpose
settle'self' | { facilitator: string; authHeaders?: () => Promise<Record<string, string>> }Pick the mode: your own relayer ('self') or a facilitator URL you choose.
relayerEVM { privateKey } or { walletClient }Required for settle: 'self' — the gas-paying wallet that broadcasts transferWithAuthorization. Distinct from payTo. Ignored in facilitator mode.
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 an EIP-3009 authorization off-chain 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 authorization locally against the trusted rail: the signature must recover to the authorizer, to must equal payTo, the value must cover the amount, the authorization must be unexpired and its on-chain nonce unused. 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.

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: { privateKey: 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.