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
quote_id, one payout. A second payout against the same quote fails.Idempotency-Key per payout turns a timeout retry into a replay instead of a double payment.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.
Pipeline it: quote, authorize if needed, execute, next row. Three patterns show up in practice, and only one holds up at size.
| Pattern | How it works | Expiry risk | Rate exposure | When it fits |
|---|---|---|---|---|
| Quote and execute per row | Each row gets its own quote, executed within seconds | Low | Each row priced at its own moment | Any batch size. The default. |
| Approve on an estimate, then quote per row | Finance approves totals from one indicative quote, with a tolerance; the run quotes and executes each row | Low | Bounded by the approved tolerance | Batches that need sign-off before money moves |
| Quote everything, then execute everything | All quotes first, all payouts after | High once the batch passes a few minutes | Locked per row, but many will expire | Tiny 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.
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.
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.
It depends on where the USDC sits. The payout endpoint is the same either way; what changes is the step before it.
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.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.For large recurring batches, the per-row approval cost on EVM is worth pricing in. Who pays gas fees breaks it down by network.
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 situation | Response |
|---|---|
| Same key, same body, original finished | Original response replayed with Idempotency-Replayed: true. No second payout. |
| Same key, different body | 422 idempotency_key_payload_mismatch |
| Same key, original still running | 409 idempotency_key_in_flight with Retry-After |
Original returned 5xx | Key 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.
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 status | Means | Action |
|---|---|---|
quoted | Quote created, not executed | Execute before expires_at or re-quote |
processing | Payout created | Wait for webhooks |
completed | Funds landed | Mark paid |
failed | Payout didn't complete | Fix the cause, requeue as a new row |
refunded | Stablecoins returned instead of paid out | Check 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.
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.
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.
Seven places blockchain payments beat bank rails: contractor payroll, remittances, B2B suppliers, marketplaces, 24/7 treasury, bill pay, and PSP payouts.
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.
The three roles behind every card program, stablecoin cards included: what each one does, who carries liability, and what to ask a provider.