Skip to content

estimateCost()

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.

// `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 is

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.

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:

FieldTypeMeaning
feeSymbolstringThe native fee coin’s ticker — ETH, BNB, SOL, TON, XLM, XRP, TRX, …
feeDecimalsnumberThe native coin’s decimals (18 EVM, 9 Solana/TON, 7 Stellar, 6 XRPL/Tron).
feestringEstimated fee in native base units (a non-negative integer string).
feeFormattedstringThe same fee, human-readable — e.g. '0.000021'.
basis'estimated' | 'heuristic'How the number was derived (see below).
detail?stringOptional note on what’s included — e.g. 'gas ~21000 @ 12 gwei'.

basis labels the estimate’s source so you can decide how much margin to keep:

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

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().

Like every read-only method, 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.

The one exception is a malformed 402 envelope: if the server returns 402 but its PAYMENT-REQUIRED body can’t be parsed, estimateCost throws an InvalidEnvelopeError — the same as quote(). That’s a broken endpoint, not a transient blip, so it surfaces rather than degrading silently.

import { InvalidEnvelopeError } from '@piprail/sdk'
try {
const result = await client.estimateCost('https://api.example.com/report')
// a bad RPC yields cost.basis: 'heuristic', not an exception
if (result) console.log(result.cost.feeFormatted, result.cost.feeSymbol)
} catch (err) {
if (err instanceof InvalidEnvelopeError) {
console.warn('The 402 response was malformed — treat this endpoint as broken.')
} else {
throw err
}
}