Run monthly payroll or supplier payouts on a schedule when each quote lasts 5 minutes: what to store, when to fire per rail, and how to never pay twice.
To schedule recurring payouts with live quotes, store the obligation (recipient, amount, which side is fixed) instead of a quote, then create and execute a fresh quote at fire time. A BlindPay payout quote lasts 5 minutes, so the schedule decides when to price each payout, not what rate it gets.
This is the calendar side of paying people every month. The run itself, row by row, is covered in how to run batch payouts with live quotes. Here the questions are what to store, when to fire, and how to make sure a cron job that runs twice still pays once.
Key takeaways
currency_type: receiver for local-currency salaries, sender for dollar contracts.Idempotency-Key header only protects a single HTTP retry.Because the quote is a price, and prices expire. A BlindPay payout quote locks the rate and fees for 5 minutes, and expires_at comes back in epoch milliseconds (payout quotes). SEPA and Faster Payments quotes can be shorter, per the cut-off times reference.
A quote also backs exactly one payout. A second execute with the same quote_id fails.
So a schedule that says "pay Ana R$ 8,000 on the 1st" is an instruction to request a quote on the 1st. The rate Ana's payout gets is the rate at that moment. If you want to understand what is in that price, how to read an FX quote breaks down the two rates every quote returns.
Store the inputs to a quote, never its outputs. A minimal schedule row:
| Field | Example | Why |
|---|---|---|
bank_account_id | ba_... | The approved recipient account |
request_amount | 800000 | Minor units, R$ 8,000.00 |
currency_type | receiver | Fixes the BRL side so the recipient gets exactly R$ 8,000.00 |
cover_fees | true | Sender pays fees on top, typical for payroll per the docs |
network, token | polygon, USDC | The funding leg the quote is priced for |
cadence | Monthly, day 1 | Payday, not fire time |
timezone | America/Sao_Paulo | Payday is a local date |
Two details matter more than they look.
Which side is fixed. On a payout quote, currency_type: sender means the amount is in the stablecoin, and receiver means it's in the fiat the bank account receives. Salaries agreed in reais belong on the receiver side. Receiver-side quotes explains the trade-off: the recipient's amount is stable, your USDC cost floats.
The timezone. Payday is a local date, and time zones change. The IANA Time Zone Database, which most servers rely on, shipped version 2026e on 2026-09-29, and that release notes Manitoba moving to permanent UTC-05 on 2026-10-31 (IANA). Store the zone name, not a fixed offset.
Fire early enough that the money lands on payday, using each rail's documented estimate and cut-off.
| Rail | BlindPay estimate | Cut-off to respect | Fire on |
|---|---|---|---|
| Pix (Brazil) | Instant | None, runs 24/7 | Payday |
| SPEI (Mexico) | Instant | Instant rail, no ACH-style cut-off | Payday |
| Transfers 3.0 (Argentina) | Instant | Instant rail, no ACH-style cut-off | Payday |
| RTP (US) | Instant | On-hold review step for USD payouts | Payday morning, or 1 business day early |
| Domestic wire (US) | About 1 business day | 3:00 PM ET | 1 to 2 business days before |
| ACH (US) | About 2 business days | 9:00 PM ET | 2 to 3 business days before |
| TED (Brazil) | About 1 business day | End of the Brazilian banking day | 1 banking day before |
| ACH Colombia | About 1 business day | Business days | 1 to 2 business days before |
| SEPA | About 1 business day | Quote can expire in under 5 minutes | 1 to 2 business days before |
Sources: the bank accounts docs for estimates and the cut-off times reference for the windows. One more thing from that reference: every USD ACH, wire, RTP, and SWIFT payout passes through on_hold for compliance review after the crypto is collected. Build that into the lead time.
Then skip holidays. The Federal Reserve's 2026 calendar closes on November 26 (Thanksgiving) and December 25 (Christmas), and the 2027 calendar opens with January 1 (Federal Reserve). A schedule for December 1 that fires ACH on November 26 just waits a day. Cut-offs, weekends, and holidays by rail has the full calendar logic.
Size limits matter less than they used to. Nacha raised the Same Day ACH limit to $1 million per payment in 2022 (Nacha), so cut-offs, not caps, usually set your fire time.
Put cycle-level uniqueness in your own database and use idempotency keys for HTTP retries. They solve different problems.
Here's the trap. BlindPay's Idempotency-Key header replays a request only if the body matches byte for byte, and it keeps keys for 24 hours (idempotency docs). Your execute body contains a quote_id. If the quote expires and you re-quote, the body changes, so the same key would return 422 idempotency_key_payload_mismatch. The header can't know that both attempts are "Ana's November salary".
So:
(schedule_id, cycle_date) with a unique constraint before quoting. A second cron run hits the constraint and stops.Idempotency-Key, for example {schedule_id}:{cycle_date}:{quote_id}. A network timeout retries the same bytes and gets the original response back with Idempotency-Replayed: true.payout.complete arrives with completed. Watch payout.update too, since failures surface there.A 409 idempotency_key_in_flight means the first attempt is still running. Wait for the Retry-After seconds, then retry. Idempotency keys in a stablecoin API goes deeper on the header.
Enough for the whole cycle at a slightly worse rate than today. If the amounts are fixed on the receiver side, your USDC need moves with the exchange rate between funding day and fire time.
Illustrative example. A team pays 40 contractors in Brazil a total of R$ 400,000.00 on the 1st, fixed in reais with cover_fees: true.
| Line | Value |
|---|---|
| Rate when funding (illustrative) | 5.40 BRL per USDC |
| USDC at that rate | 400,000 / 5.40 = 74,074.07 |
| Rate on payday if BRL strengthens | 5.25 BRL per USDC |
| USDC at payday rate | 400,000 / 5.25 = 76,190.48 |
| Gap | 76,190.48 - 74,074.07 = 2,116.41 USDC (2.9%) |
| Fees, sender-paid | Added per quote, read from each response |
Funding exactly 74,074.07 USDC would leave the last few contractors unpaid. A 3% buffer covers this move, and the rest stays in the wallet for next month. How much USDC to hold for payouts has the sizing formula.
If you fix amounts on the sender side instead, the USDC total is known in advance and the recipients absorb the movement. That's a business choice. The FX gap between a sale and a payout walks through who should carry it.
Re-quote, never retry the old quote. An expired quote is rejected, not executed at a new rate, so the job requests a new quote and executes that one.
Failures follow the same rule. A payout that ends failed or refunded leaves the cycle row open; the job flags the line, a person checks the bank details, and the retry gets a fresh quote. If stablecoins were already collected, refunds return them to the quote's refund wallet, or to the sending wallet when none is set. On Pix and other non-USD local rails that refund is automatic, while USD payouts and payouts whose review times out are refunded after an operator review.
Test both paths before the first live cycle. On a development instance, a quote for 66600 forces failed and 77700 forces refunded (payouts docs). Handling expired quotes in TypeScript and Python has the re-quote loop.
Write one schedule row for a single Pix recipient on a development instance, fire it from a cron job twice in a row, and confirm the second run stops at your unique constraint. The payout quotes docs have the request body.
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.