Paying the upto (metered) rail
Introduction
Section titled “Introduction”By default a PipRailClient pays only PipRail’s native
onchain-proof rail, and can opt into the standard exact scheme (a
fixed amount someone else broadcasts). The ratified upto scheme is the metered counterpart:
the server bills a variable amount up to a ceiling. You authorize the ceiling once; the merchant serves
the resource, meters the actual usage, and settles exactly that (actual ≤ max).
It’s the rail behind usage-priced APIs — per-LLM-token billing, per-query data, pay-per-compute — where the price isn’t known until the work is done.
Opting in
Section titled “Opting in”upto is off by default. Enable it on the constructor (or per call), exactly like exact:
import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'base', wallet: { key: process.env.AGENT_KEY! }, schemes: ['onchain-proof', 'upto'], // pay PipRail rails AND standard metered (upto) rails})What the buyer does: the client signs a Permit2 authorization for the MAX once — zero gas, and it
broadcasts nothing. (After a one-time approve(Permit2) the SDK does lazily the first time you pay a
token, every later payment is gasless.) The merchant then self-settles the actual it metered. You
sign; you never broadcast.
Budget against the MAX (the headline)
Section titled “Budget against the MAX (the headline)”A upto payment is budgeted, planned, and policy-gated against the signed MAX — not the actual the
merchant later settles. This is the one rule to internalise, and it is a deliberate merchant-proof
design, not an approximation.
The reason: the buyer signs an authorization that lets the merchant settle anything up to the max, so the only amount the buyer can prove an upper bound on is the max itself. The merchant’s claimed actual is untrusted — a malicious merchant could settle the full max on-chain yet report a tiny settled amount. So:
- Per-payment policy (
maxAmount,tokens,chains) andplanPayment/canAffordgate on the MAX. Auptorail is “payable” only when the full ceiling fits your leash and your token balance — the server may charge all of it. (Because the buyer signs and the merchant broadcasts, only the token balance gates payability — buyer gas is ~0, so anINSUFFICIENT_GASblocker never applies on anuptorail.) - After settle, the cumulative caps debit the authorized MAX. The lifetime
maxTotal, the cross-token grand totalmaxTotalPerDenom, and the rollingwindowTotalall advance by the MAX, so an under-reporting merchant can never loosen the leash past your real on-chain exposure. - The settled actual is surfaced for reconciliation only. Each
SpendRecordcarriessettledBase/settledFormatted— the merchant-claimed actual, clamped to≤the MAX so even the informational figure can’t be inflated. It equals the receipt’samount. It is display-only — it never affects a cap.
Worked example
Section titled “Worked example”Sign a MAX of 0.50; the server meters and settles 0.12:
const client = new PipRailClient({ chain: 'base', wallet: { key: process.env.AGENT_KEY! }, schemes: ['onchain-proof', 'upto'], policy: { maxTotal: '5.00' }, // lifetime cap, USDC})
await client.fetch('https://api.example.com/complete') // signs MAX 0.50; server settles 0.12
const summary = client.spent()const usdc = summary.byAsset.find((a) => a.symbol === 'USDC')console.log(usdc?.totalFormatted) // → '0.50' (the budget counts the MAX)
const last = summary.records.at(-1)!console.log(last.amountFormatted) // → '0.50' (budgeted MAX — the cap-bearing figure)console.log(last.settledFormatted) // → '0.12' (informational actual, clamped ≤ MAX)After this one payment, maxTotal has 4.50 remaining — debited by the MAX 0.50, not the 0.12
the merchant settled. The 0.12 is yours to reconcile against the chain, never to relax the budget.
The dual-rail preference
Section titled “The dual-rail preference”When a gate offers both onchain-proof and upto and you have both enabled, the client prefers
onchain-proof — it gathers candidates onchain-proof first, then exact, then upto, and picks
the first one your policy allows, so the default selection is unchanged. To pay a gate over upto
specifically, enable only upto (drop onchain-proof from schemes):
// force the metered rail on a dual-rail gate:await client.fetch(url, { schemes: ['upto'] })(Unlike exact, there is no autoRoute shortcut that prefers upto — its budgeted cost is the MAX,
not ~0, so it doesn’t automatically win a cheapest-rail race.)
quote, plan, and pay
Section titled “quote, plan, and pay”quote(), planPayment(), and the
fetch/get/post pay path all honour the enabled schemes and analyse upto rails when upto is on.
On an upto rail every read is against the MAX (the amount, the policy check, the affordability
check). Paying is the same call as ever — the 402 is handled transparently:
const url = 'https://api.example.com/complete'
const plan = await client.planPayment(url) // analyses the upto rail against the MAXif (!plan) { await client.fetch(url) // not gated — fetch it for free} else if (plan.payable) { // payable ⇒ the full MAX fits your leash + balance const res = await client.fetch(url, { schemes: ['upto'] }) const data = await res.json() // the gated response, paid for via upto} else { console.log(plan.fundingHint) // one-line: what to top up (sized to the MAX)}planPayment() returns null when the URL isn’t payment-gated, so null-guard it before reading
payable.
What you get back
Section titled “What you get back”client.fetch returns the merchant’s normal Response once the payment settles. The settlement
receipt has scheme: 'upto' and an amount equal to the
ACTUAL the merchant settled (a zero-charge settle has transaction: ''). The client records that
actual on the spend record’s settledBase / settledFormatted (clamped ≤ MAX) — while the budget
already debited the MAX. Read the latest receipt with client.lastReceipt().
Failure modes
Section titled “Failure modes”The upto pay path is the conservative sign-once path, like exact: the buyer signs one Permit2
authorization (a fresh nonce) 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 Permit2 nonce as.ref— the merchant may have already settled, so verify on-chain and never re-pay. - A definitive rejection (
success: false) throwsMaxRetriesExceededError— fix the cause, then re-present the same signed authorization (the same nonce), 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.
import { PipRailClient, PaymentTimeoutError, MaxRetriesExceededError } from '@piprail/sdk'
try { await client.fetch(url, { schemes: ['upto'] })} catch (err) { if (err instanceof PaymentTimeoutError || err instanceof MaxRetriesExceededError) { // .ref is the Permit2 nonce — check it on-chain, re-present the SAME authorization, do NOT re-pay console.log('recover with this authorization nonce:', err.ref) } else { throw err }}If a 402 offers only an upto rail and your bound family can’t pay it (a non-EVM family, a native
coin, or a contract / EIP-1271 / EIP-7702 signer), the client throws
UnsupportedSchemeError rather than signing something that can’t settle.
See also
Section titled “See also”- The upto rail — metered billing (seller) — how the gate meters and settles the actual.
- Spend controls — payment policy and total budget — the caps the MAX debits, and why that’s merchant-proof.
- PipRailClient — the client,
schemes, andspent()/budget().