Skip to content

quote()

quote(url) answers the first question an agent asks at a 402: what does this cost, and can I trust the number? It does the initial request, and if the resource is payment-gated it returns a PipRailQuote — the amount in the token’s true decimals, the chain, the recipient, and whether your policy would allow it. No funds move. It returns null when the URL isn’t gated (no 402).

It’s the first of the read-only trio: quote() learns the price, estimateCost() adds the gas, and planPayment() decides whether the wallet can actually settle it.

import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({
chain: 'base',
wallet: { privateKey: process.env.AGENT_KEY! },
})
const url = 'https://api.example.com/report'
const quote = await client.quote(url)
// → { amountFormatted: '0.10', symbol: 'USDC', network: 'eip155:8453',
// payTo: '0xYourWallet', withinPolicy: true, recognized: true, … } | null
if (quote && quote.withinPolicy) {
await client.fetch(url) // pay it — we checked the price
}

If quote is null, the URL returned something other than a 402 — it isn’t paid, so just fetch it.

interface PipRailQuote {
url: string
chain: ChainSelector // the chain this client is configured for
network: Caip2
asset: string // 0x… / mint / jetton master / CODE:ISSUER / 'native'
amount: string // base units — what actually transfers
amountFormatted: string // human-readable, e.g. '0.10'
decimals: number
symbol?: string
payTo: string
description?: string
maxTimeoutSeconds: number
recognized: boolean // did the SDK know this asset (and trust its decimals)?
symbolMismatch: boolean // challenge's symbol disagrees with the SDK's real one
withinPolicy: boolean // would the configured policy allow paying this?
policyReason?: string // prose, only when withinPolicy === false
policyCode?: PolicyDenyCode // typed reason, only when withinPolicy === false
}

True decimals — the SDK never trusts the server

Section titled “True decimals — the SDK never trusts the server”

A malicious or buggy server can state any decimals it likes in the challenge, which would let it misrepresent what 0.10 means. PipRail doesn’t take the server’s word: when it recognises the asset (recognized: true), decimals, symbol, and amountFormatted come from the SDK’s own on-chain truth, not the challenge. For an honest server they match; for a deceptive one, your agent sees the real figure.

const quote = await client.quote(url)
if (!quote) return // not a 402 — nothing to price
console.log(`${quote.amountFormatted} ${quote.symbol}`) // '0.10 USDC' — re-derived from TRUE decimals
console.log(quote.recognized) // true → decimals/symbol are the SDK's

symbolMismatch is true when the challenge’s stated symbol disagrees with the symbol the SDK knows for that asset (only ever set for a recognised token). It’s a red flag worth surfacing — a server claiming to charge “USDC” against a token whose real symbol is something else.

const quote = await client.quote(url)
if (quote?.symbolMismatch) {
// the on-chain symbol differs from what the challenge claims — surface it, don't auto-pay
}

A mismatch doesn’t block anything on its own; fetch() still pays the amount in true decimals. planPayment() surfaces the same signal as a SYMBOL_MISMATCH warning.

If you constructed the client with a policy, quote() evaluates this payment against it and reports the verdict without enforcing it — withinPolicy is the boolean, and when it’s false you also get a human policyReason and a typed policyCode. With no policy set, withinPolicy is always true.

const quote = await client.quote(url)
if (quote && !quote.withinPolicy) {
console.log(quote.policyCode, '', quote.policyReason) // e.g. 'MAX_AMOUNT' — over per-payment cap
}

policyCode is one of the PolicyDenyCode values, so you can branch without parsing prose:

policyCodeWhy the policy would refuse it
MAX_AMOUNTOver the per-payment ceiling.
MAX_TOTALWould exceed the lifetime spend cap.
CHAINThe chain isn’t on your allowlist.
TOKENThe token isn’t on your allowlist.
HOSTThe resource host isn’t allowed.
UNKNOWN_TOKENAn unrecognised token, refused by policy.
SESSION_EXPIREDThe session’s time envelope has ended.
WINDOW_TOTALThe rolling-window spend cap is exhausted.

quote() issues exactly one GET (or your chosen method via init) and, on a 402, parses the challenge to build the quote. It signs nothing and broadcasts nothing — it can’t move funds. Pass a RequestInit to control the probe request:

const quote = await client.quote(url, { method: 'POST', headers: { accept: 'application/json' } })

For the gas estimate to go with the price, reach for estimateCost(); to know whether the wallet can actually settle it, planPayment().