How to schedule recurring payouts when every quote expires in 5 minutes

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

  • Never schedule a quote. Schedule the obligation and quote at fire time.
  • Store which side is fixed: currency_type: receiver for local-currency salaries, sender for dollar contracts.
  • Fire time depends on the rail. Pix and SPEI can go on payday; ACH has to go business days earlier.
  • Cycle-level "pay once" lives in your database. The Idempotency-Key header only protects a single HTTP retry.
  • Fund the wallet before the run, with a buffer for rate movement between quote time and payday.

Why can't you schedule the quote itself?

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.

What should a recurring payout record store?

Store the inputs to a quote, never its outputs. A minimal schedule row:

FieldExampleWhy
bank_account_idba_...The approved recipient account
request_amount800000Minor units, R$ 8,000.00
currency_typereceiverFixes the BRL side so the recipient gets exactly R$ 8,000.00
cover_feestrueSender pays fees on top, typical for payroll per the docs
network, tokenpolygon, USDCThe funding leg the quote is priced for
cadenceMonthly, day 1Payday, not fire time
timezoneAmerica/Sao_PauloPayday 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.

When should each rail fire?

Fire early enough that the money lands on payday, using each rail's documented estimate and cut-off.

RailBlindPay estimateCut-off to respectFire on
Pix (Brazil)InstantNone, runs 24/7Payday
SPEI (Mexico)InstantInstant rail, no ACH-style cut-offPayday
Transfers 3.0 (Argentina)InstantInstant rail, no ACH-style cut-offPayday
RTP (US)InstantOn-hold review step for USD payoutsPayday morning, or 1 business day early
Domestic wire (US)About 1 business day3:00 PM ET1 to 2 business days before
ACH (US)About 2 business days9:00 PM ET2 to 3 business days before
TED (Brazil)About 1 business dayEnd of the Brazilian banking day1 banking day before
ACH ColombiaAbout 1 business dayBusiness days1 to 2 business days before
SEPAAbout 1 business dayQuote can expire in under 5 minutes1 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.

How do you make sure a cycle pays exactly once?

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:

  1. Cycle row. Insert one row per (schedule_id, cycle_date) with a unique constraint before quoting. A second cron run hits the constraint and stops.
  2. Attempt key. Each execute call gets its own 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.
  3. Close from webhooks. Mark the cycle row done when 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.

How much should you fund before the run?

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.

LineValue
Rate when funding (illustrative)5.40 BRL per USDC
USDC at that rate400,000 / 5.40 = 74,074.07
Rate on payday if BRL strengthens5.25 BRL per USDC
USDC at payday rate400,000 / 5.25 = 76,190.48
Gap76,190.48 - 74,074.07 = 2,116.41 USDC (2.9%)
Fees, sender-paidAdded 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.

What should the job do when a quote expires or a payout fails?

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.

What to do next

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.

FAQ