estimateCost()
Introduction
Section titled “Introduction”estimateCost(url) answers the second question an autonomous agent must ask: not just what
does this cost to pay, but what does it cost to send? The payment leaves the wallet in the
token (USDC, USDT, native coin); the gas to broadcast it leaves in the chain’s native coin
(ETH, SOL, TRX, …). Those are two different numbers, and a 402’s price tells you only the first.
estimateCost returns both. It does the initial request and, if it’s a 402, gives you a
PipRailQuote plus a CostEstimate. It reads RPC where that’s cheap,
but it never pays — and returns null when the URL isn’t payment-gated.
Basic use
Section titled “Basic use”// `client` is the PipRailClient you configured earlier — see /making-payments/piprail-client/const result = await client.estimateCost('https://api.example.com/report')
if (result) { console.log(result.quote.amountFormatted, result.quote.symbol) // '0.10' 'USDC' — the payment console.log(result.cost.feeFormatted, result.cost.feeSymbol) // '0.000021' 'ETH' — the gas}// → null when the URL isn't payment-gated (no 402)// → { quote: PipRailQuote, cost: CostEstimate } when it isThe PipRailCostQuote
Section titled “The PipRailCostQuote”The return value pairs the priced requirement with the gas to settle it:
interface PipRailCostQuote { quote: PipRailQuote // what the payment is — amount, token, chain, recipient, policy cost: CostEstimate // the network fee (gas) to send it, in the native coin}quote is exactly what quote() returns. cost is the new piece.
The CostEstimate
Section titled “The CostEstimate”Every driver computes its chain’s fee — EVM gas × price, Solana lamports, Tron energy × price,
XRPL drops — as a bigint of native base units, then shapes it through util/cost.ts’s
nativeCost() helper, so the fields are identical on every chain:
| Field | Type | Meaning |
|---|---|---|
feeSymbol | string | The native fee coin’s ticker — ETH, BNB, SOL, TON, XLM, XRP, TRX, … |
feeDecimals | number | The native coin’s decimals (18 EVM, 9 Solana/TON, 7 Stellar, 6 XRPL/Tron). |
fee | string | Estimated fee in native base units (a non-negative integer string). |
feeFormatted | string | The same fee, human-readable — e.g. '0.000021'. |
basis | 'estimated' | 'heuristic' | How the number was derived (see below). |
detail? | string | Optional note on what’s included — e.g. 'gas ~21000 @ 12 gwei'. |
basis — how trustworthy is the number
Section titled “basis — how trustworthy is the number”basis labels the estimate’s source so you can decide how much margin to keep:
basis | Meaning |
|---|---|
'estimated' | Derived from a live RPC read (EVM gas price, XRPL fee) — sharp. |
'heuristic' | A typical-cost constant, used when an RPC read would be slow or unavailable. |
const result = await client.estimateCost(url)if (result && result.cost.basis === 'heuristic') { // the gas figure is a ballpark — keep a wider native-coin margin}Budgeting payment plus gas
Section titled “Budgeting payment plus gas”Together, the two numbers let an agent reason about the total before any funds move: the token amount must be affordable, and there must be enough native coin left over for gas.
const result = await client.estimateCost('https://api.example.com/report')if (result) { console.log(`Pay ${result.quote.amountFormatted} ${result.quote.symbol}`) console.log(`Gas ~${result.cost.feeFormatted} ${result.cost.feeSymbol} (${result.cost.basis})`)}// → Pay 0.10 USDC// → Gas ~0.000021 ETH (estimated)This is the middle step of the read-only trio: quote() learns the
price, estimateCost() adds the gas, and planPayment() checks
your balances against both and tells you whether the whole thing is settleable. If you want the
verdict — not just the numbers — reach for planPayment().
Returns null when there’s nothing to cost — and what it does throw
Section titled “Returns null when there’s nothing to cost — and what it does throw”estimateCost is safe to call speculatively. A transient RPC issue falls back to a 'heuristic'
constant rather than raising, so a flaky endpoint degrades the estimate’s sharpness instead of
crashing your agent.
It returns null in exactly two cases: a non-402 response (the URL isn’t payment-gated),
or a malformed / unparseable 402 envelope (an unparseable PAYMENT-REQUIRED body, or a
parseable-but-malformed rail with a bad amount/asset/decimals). So unlike
quote() — which throws an InvalidEnvelopeError on a malformed
402 — estimateCost degrades a bad envelope to null, and a caller just checks for it.
It is not unconditionally throw-free, though. When the 402 is well-formed but offers no rail
this client can pay on its chain + enabled schemes, estimateCost throws
NoCompatibleAcceptError (or UnsupportedSchemeError) — a deliberate
“this 402 isn’t for you” signal, since that’s a routing fact, not a cost-read failure. Guard it the
way you would any payment call:
import { NoCompatibleAcceptError } from '@piprail/sdk'
try { const result = await client.estimateCost('https://api.example.com/report') if (result) { // a bad RPC yields cost.basis: 'heuristic', not an exception console.log(result.cost.feeFormatted, result.cost.feeSymbol) } else { // null = non-402, or a malformed/un-costable 402 envelope — nothing to estimate console.warn('No cost to estimate (not gated, or a malformed 402).') }} catch (err) { if (err instanceof NoCompatibleAcceptError) { // the 402 is well-formed but offers no rail this client can pay — "not for you" console.warn('This 402 offers nothing this client can pay:', err.message) } else throw err}