Measure quote API latency at p50 and p99, set timeouts that fit a 5-minute or 10-second quote window, cache rates safely, and retry without duplicates.
A quote API should answer well inside the time your user will wait, and your client timeout should come from measured tail latency, not a guess. The number that matters is not the average. It's the p99, and how much of the quote window the slowest call, a retry, and the approval step eat before you execute.
Most teams never look at this until a payout fails with an expired quote and nobody can say where the five minutes went. The math is simple once it's on a whiteboard. This page puts it there. For why the window exists at all, start with how on/off-ramp providers make money, since every locked second is risk somebody prices.
Key takeaways
expires_at (epoch milliseconds) from every response instead of hardcoding a TTL.Idempotency-Key, resending the exact same body.Because the window is shared. The clock starts when the provider creates the quote, and everything after that runs against it: the response traveling back, your UI rendering the amount, a human approving, and the execute call.
On BlindPay, payin and payout quotes default to 5 minutes. OTC payin quotes expire in 10 seconds, and SEPA payout quotes can be shorter than 5 minutes because the rail's own deadline can be tighter (cut-off times). That's why the docs say to read expires_at rather than assume a fixed lifetime.
A 2-second slow response is 0.7% of a 5-minute window. Rounding error. The same 2 seconds is 20% of a 10-second OTC window. Not great. Short windows exist because the provider is holding a price in a moving market; why a locked quote costs money explains that trade.
Measure p50, p95, and p99 per endpoint, and track failed calls separately from successful ones. Averages lie about payments.
Google's SRE book makes the point with an example: a service with a 100 ms average can still have 1% of requests take 5 seconds, and when several backends feed one page, "the 99th percentile of one backend can easily become the median response of your frontend" (Google SRE book). It also warns that "a slow error is even worse than a fast error", so don't filter errors out of your latency charts.
What to record on every quote call:
expires_at.The fourth one is the metric people skip. It tells you directly whether latency plus human time is eating the window. Bucket all of it into histograms, the format the SRE book recommends, so the tail is visible.
Start from a target false-timeout rate, then read the percentile. AWS describes the method for its own services: "we choose an acceptable rate of false timeouts (such as 0.1%). Then, we look at the corresponding latency percentile on the downstream service (p99.9 in this example)" (AWS Builders' Library).
Then cap it with a budget tied to the window. A timeout is only useful if a retry can still finish inside the quote's life.
| Call | What it does | Timeout guidance | Retry? |
|---|---|---|---|
Indicative rate (/payin-quotes/fx) | Preview price, reserves nothing | Short; drop stale previews | Yes, freely |
| Payin or payout quote | Locks rate and fees for the window | Near your measured p99.9 | Yes, same body |
| OTC payin quote | Locks a BRL price for 10 seconds | Small slice of 10 seconds | Only if time remains |
| Execute payout or payin | Moves money | Near p99.9, plus margin | Only with Idempotency-Key |
If your measured p99.9 for a quote call is longer than you can afford on a short window, the fix is not a shorter timeout. It's a flow change: request the quote at the moment of confirmation, not when the page loads.
Cache rates, not commitments. The two calls behave differently.
An indicative rate reserves nothing. BlindPay's POST /payin-quotes/fx returns a rate and amount "without creating a payin quote", and the docs say plainly that its numbers "aren't guaranteed to match the quote you get" a moment later (payin quotes). Caching that for a few seconds behind a typing field is fine. Indicative rate vs binding quote covers the two-call pattern.
A binding quote is different. You can hold its ID until expires_at to execute it once. You can't reuse it for another amount, another recipient, or after expiry. Two mistakes show up often:
Rule of thumb: the number on the confirm button must come from a quote whose expires_at is still in the future when the click lands.
With idempotency keys, backoff, and jitter. Retries are selfish: AWS notes that "when a client retries, it spends more of the server's time to get a higher chance of success", and that retries "can make the overload worse" (AWS Builders' Library). So space them out with exponential backoff and add jitter.
The money side needs more than spacing. On BlindPay, any authenticated POST, PUT, PATCH, or DELETE under /v1/* accepts an Idempotency-Key header of up to 255 characters (idempotency):
422 idempotency_key_payload_mismatch.409 idempotency_key_in_flight with a Retry-After header.5xx releases the key, so the retry runs fresh.Use one key per attempt to execute a specific quote. A new quote gets a new key. Idempotency keys for stablecoin APIs walks through the failure modes in depth.
Illustrative example. A payout flow with one human approval, measured on a team's own servers. The numbers are made up for the walkthrough, not BlindPay figures.
| Step | Time used | Time left on a 5-minute quote | Time left on a 10-second quote |
|---|---|---|---|
| Quote request (p99) | 1.5 s | 298.5 s | 8.5 s |
| Render amount to approver | 0.5 s | 298.0 s | 8.0 s |
| Human approval | 45 s | 253.0 s | Expired |
| Execute call (p99) | 1.5 s | 251.5 s | n/a |
The 5-minute flow finishes with four minutes to spare even on the slow path. The 10-second flow can't fit a human at all. That's the real lesson for OTC: automate the confirmation, or have the approver approve the amount range before the quote is requested.
Now add one timed-out quote request with a 3-second timeout and a retry. On the 5-minute quote, nothing changes. On the 10-second quote, the retry alone leaves about 5 seconds for everything else.
Three habits cover most of it.
Teams comparing providers should run the same harness against each one from the same region, at the same hours, for a week. Latency differences between providers are often smaller than the differences between your own regions.
Instrument one quote call today: log start, end, status, expires_at, and the time left when you executed. A week of that data tells you your real timeout and whether your approval flow fits the window. The payout quotes reference has every field you need to log.
Seven places blockchain payments beat bank rails: contractor payroll, remittances, B2B suppliers, marketplaces, 24/7 treasury, bill pay, and PSP payouts.
Push means the payer sends an ACH transfer. Pull means you debit a connected account. How each funds a USD to USDC on-ramp, the fees, and the return risk.
AP2, ACP, and x402 each verify that an AI agent had permission to spend. Here is what every protocol covers, who backs it, and the reconciliation gap none of them close.