How to run batch payouts with live quotes: one quote per payout, expiry, and rate drift

How to pay 100+ recipients from USDC with live quotes: quote and execute per payout, size the funding, handle expiry and retries, and track the batch.

To run a batch of payouts with live quotes, treat each payout as its own quote-then-execute pair. A quote backs exactly one payout and expires after 5 minutes by default, so a batch of 120 recipients is 120 quotes, each executed right after it's created. Fund the wallet first, send an idempotency key per payout, and track each one to a terminal status.

The batch is a loop. What makes it production-grade is everything around the loop.

Key takeaways

  • One quote_id, one payout. A second payout against the same quote fails.
  • Don't quote the whole batch up front. Quotes expire in 5 minutes by default; pipeline quote and execute per row.
  • Rows quoted later get later rates. Fund with a buffer for drift across the run.
  • An Idempotency-Key per payout turns a timeout retry into a replay instead of a double payment.
  • A batch is finished when every payout id has a terminal status, not when the loop ends.

Why can't one quote cover a whole batch?

Because a quote is a price for one conversion to one bank account. It names one bank_account_id, one amount, and one fee calculation, and it backs one payout.

That is a feature. Every recipient in the batch gets a firm number: the receiver_amount on their quote is what lands. A single blended rate for 120 people would make each recipient's amount an estimate until the batch finished.

It also means the batch is only as good as its slowest row. If row 87 sits in a queue for six minutes, its quote expires, and you re-quote it. Nothing executes at a stale rate. The glossary defines quote window, re-quote, and binding quote if you need the vocabulary.

How should you structure a batch run?

Pipeline it: quote, authorize if needed, execute, next row. Three patterns show up in practice, and only one holds up at size.

PatternHow it worksExpiry riskRate exposureWhen it fits
Quote and execute per rowEach row gets its own quote, executed within secondsLowEach row priced at its own momentAny batch size. The default.
Approve on an estimate, then quote per rowFinance approves totals from one indicative quote, with a tolerance; the run quotes and executes each rowLowBounded by the approved toleranceBatches that need sign-off before money moves
Quote everything, then execute everythingAll quotes first, all payouts afterHigh once the batch passes a few minutesLocked per row, but many will expireTiny batches only

The second pattern is how most payroll teams run. Someone approves "about 97,800 USDC, plus or minus 1%" in the morning. The run quotes each row live. If the total drifts past the tolerance, the run pauses and asks again.

How much USDC should you fund before the run?

Enough for the total at current rates, plus a buffer for drift and fees. You won't know the exact total until the last row is quoted, because every row is priced at its own moment.

Illustrative example. A company pays 120 contractors in Mexico over SPEI, each a fixed $15,000.00 MXN, quoted on the receiver side. The numbers below are made up for the arithmetic, not BlindPay pricing.

  • Batch total: 120 x 15,000 = $1,800,000.00 MXN.
  • Effective rate on the first quote: 18.4075 MXN per USDC.
  • USDC for the batch at that rate: 1,800,000 ÷ 18.4075 = 97,786.23 USDC.
  • Suppose the peso strengthens 0.3% during a long run, so the effective rate falls to 18.3523. Each row then needs 15,000 ÷ 18.3523 = 817.34 USDC instead of 814.89.
  • If every row priced at the later rate, the batch would need 1,800,000 ÷ 18.3523 = 98,080.47 USDC. That's 294.24 USDC more than the first estimate.

A 0.5% to 1% buffer covers a move like that. Anything left stays in your wallet. Short runs shrink the problem: the less time between the first and last quote, the less room for drift. For how big that float should be month to month, see no pre-funding in a stablecoin API.

How do you authorize funds for each payout?

It depends on where the USDC sits. The payout endpoint is the same either way; what changes is the step before it.

  • External EVM wallet. Each quote returns a contract payload. You call approve on the token contract for that quote's exact amount, then execute with quote_id and sender_wallet_address. That's one on-chain approval per row, so 120 rows means 120 approvals and their gas. The EVM payout guide has the full script.
  • Managed wallet. No client-side signing. You pass the wallet address as sender_wallet_address and execute. BlindPay-managed wallets are in beta and custodial, so this path trades the approval step for BlindPay holding the balance.
  • Solana and Stellar. Each has its own signing step per payout: a token delegation on Solana, an authorize-and-sign XDR on Stellar.

For large recurring batches, the per-row approval cost on EVM is worth pricing in. Who pays gas fees breaks it down by network.

How do you make retries safe?

Send an Idempotency-Key header on every payout call, one key per row, and resend the byte-identical body on retry. That's the whole defense against double payments when a request times out.

BlindPay's idempotency rules are specific:

Retry situationResponse
Same key, same body, original finishedOriginal response replayed with Idempotency-Replayed: true. No second payout.
Same key, different body422 idempotency_key_payload_mismatch
Same key, original still running409 idempotency_key_in_flight with Retry-After
Original returned 5xxKey released, so the retry runs fresh

Two practical rules. Derive the key from the batch and row, for example batch-2026-10-04-row-087, so a restarted job reuses it. And serialize the body once and keep the bytes: re-serializing JSON with a different key order counts as a different body. The IETF's Idempotency-Key header draft describes the same pattern across APIs, and the idempotency keys explainer covers it in more depth.

If a row's quote expired before the retry, the retry should request a new quote first. A new quote means a new payout body, so give it a new key.

How do you track a batch to the end?

By payout id, through webhooks. The loop ending tells you every request was sent, not that every peso landed.

BlindPay fires payout.new when a payout is created and its source funds are captured, payout.update on intermediate steps (including reaching failed), and payout.complete when a payout is confirmed at the destination or refunded. The webhook events reference lists them.

Keep a row-level status table and close the batch only when every row is terminal:

Row statusMeansAction
quotedQuote created, not executedExecute before expires_at or re-quote
processingPayout createdWait for webhooks
completedFunds landedMark paid
failedPayout didn't completeFix the cause, requeue as a new row
refundedStablecoins returned instead of paid outCheck the bank details, requeue

On Pix and other non-USD local rails, a payout that can't be delivered after the stablecoins are collected is refunded automatically to the quote's refund wallet, or to the sending wallet when none is set. Recipients in Mexico can also check a SPEI transfer through Banco de México's CEP lookup, which helps when a contractor asks where their money is. More on the states in stablecoin payout statuses explained.

What should you test before the first real batch?

Run the whole loop on a development instance with test bank accounts and USDB, BlindPay's test stablecoin. Payouts there are free.

Test the unhappy paths on purpose: let one quote expire, retry one payout with the same idempotency key, and kill the job halfway and restart it. Development instances are capped at roughly 100 requests per minute, so pace a large test batch or you'll see 429 with a Retry-After header. The sandbox vs production guide lists what else differs.

Since SPEI runs around the clock per Banco de México, a Mexico batch doesn't have to wait for a business day. For a single exact-amount payout, start with paying an exact BRL, MXN, or EUR amount, and for marketplace-style runs see marketplace payouts in Latin America.

What to do next

Take last month's payout file, pick ten rows, and run them through a development instance with the quote-and-execute loop, one idempotency key per row. Then read the payouts reference for the status lifecycle before you scale it to the full batch.

FAQ