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 it 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), so it’s 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, so 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}