Payouts

Mass payouts: pay thousands of recipients in one idempotent batch

Once a week someone on your finance team opens a spreadsheet of a few hundred recipients, splits it across wallets, fires the transfers one at a time, and prays the script doesn't crash halfway. halfin mass payouts replace that ritual: submit the whole recipient set as one batch, each line carries its own idempotency key, and a retry after a timeout pays each recipient exactly once. You get a single pass/fail picture instead of a row-by-row guessing game.

01

The weekly payout run is where money quietly goes wrong

Paying many people in crypto is deceptively easy to start and genuinely hard to do safely. The first version is a loop over a CSV: read a row, send a transfer, move on. It works until the script dies on row 240 of 600 — a dropped RPC connection, a rate limit, an out-of-gas wallet — and now nobody knows which recipients already received funds. Re-running the whole file risks paying the first 239 people twice. Picking up by hand from row 240 risks skipping someone. Neither option is auditable, and both happen under time pressure on payout day.

The failure mode that actually hurts is the silent double-pay. Network calls time out and return ambiguously: the request may have landed on-chain even though your client never saw the response. A naive retry treats that as a fresh send and pushes a second transaction. At scale you don't notice until a recipient flags it or your treasury reconciliation comes up short — by which point the funds are gone and final.

halfin's mass payout model removes the ambiguity. Every payout in a batch carries a caller-supplied idempotency key. If you submit the same key twice — because a request timed out, your worker restarted, or someone re-ran the file — halfin returns the original payout instead of creating a new one. The batch becomes safe to retry from the top, every time, which is exactly the property a payout run needs and a hand-rolled loop never has.

02

One batch, one idempotency key per recipient, one verdict

A mass payout is a set of single payouts you submit together and reason about as a unit. Each recipient line specifies a currency, an amount as a string, and a destination address, plus its own idempotency key — typically derived from your own ledger, like the affiliate's account id joined with the payout period. That derivation is the whole trick: because the key is deterministic, re-submitting the batch reproduces the same keys, and halfin matches each one to the payout it already created.

Partial failure is treated as the normal case, not an exception. Some lines will be rejected up front — a malformed address, an amount below a chain's dust threshold, a currency the destination can't receive — and those are reported per-line so the rest of the batch still goes through. You fix the rejected rows and re-submit the whole file; the lines that already succeeded are no-ops, and only the corrected ones execute. There is no step where a single bad row blocks two hundred good ones.

Payouts don't move funds the instant you call the API. Each one enters a pending-approval state and is released from the dashboard, so the batch you POST is a proposal, not an irreversible action. That keeps the high-throughput programmatic path and the human control point separate: your integration can stage thousands of payouts unattended, and a treasurer still signs off before anything leaves the balance.

03

What a payout line carries

Every recipient in a batch is described by the same small set of fields. Amounts are always strings — never floating-point numbers — so a value like a USDT payout is transported exactly, with no binary-rounding drift between your books and the chain. Pick the currency and network per line: pay an Asia-based contractor in USDT on Tron, a US affiliate in USDC on Ethereum or Base, and a trader in native SOL, all in the same submission.

FieldTypeNotes
currencystringAsset + network, e.g. USDT (TRC-20 / ERC-20 / Solana), USDC (ERC-20 / Solana / Base), BTC, ETH.
amountstringDecimal as a string. Never a JS number — money stays exact end to end.
destinationstringRecipient address; validated for the chosen network before the payout is accepted.
idempotency_keystringCaller-supplied, deterministic per recipient+run. Re-submitting returns the original payout.
04

Submitting a batch from code

There is no separate "batch" endpoint to learn: a mass payout is a fan-out over the single-payout API, where idempotency keys make the loop safe to interrupt and re-run. Derive each key from your own records so the same recipient in the same period always maps to the same key. If the process dies mid-run, you re-run the entire loop — already-created payouts come back unchanged, and only the missing ones are created.

The example below walks a recipient list and submits each line with its own key. Because each call is idempotent, wrapping the whole thing in a retry is correct rather than dangerous — the second pass converges on exactly one payout per recipient.

# Fan out a payout run; each line is idempotent, so re-running is safe.
while IFS=, read -r account currency amount destination; do
  curl -sS -X POST https://api.thehalfin.com/api/v1/payouts \
    -H "Content-Type: application/json" \
    -H "X-API-Key: $HALFIN_API_KEY" \
    -d "{
      \"currency\": \"$currency\",
      \"amount\": \"$amount\",
      \"destination\": \"$destination\",
      \"idempotency_key\": \"payout-2026-06-$account\"
    }"
done < recipients.csv
# Re-run the same file after a crash: keys collide, nobody is paid twice.
05

Pick the chain each recipient actually wants

Recipients don't share a preference, and forcing everyone onto one network means rejected payments and addresses that can't receive the asset you sent. A trader settling in USDT may want it on Tron; a US-facing affiliate may need USDC on Ethereum for their own accounting; a creator might prefer Solana for speed. halfin lets each line pick its own currency and network, so a single run can span all of them.

Settlement is reorg-aware and uses per-chain confirmation thresholds, so a payout reported as complete has actually settled to the depth that chain requires — not just been broadcast. That matters most at the tail of a large batch, where you want the per-recipient status to mean "done", not "submitted and hopefully fine".

  • Stablecoins: USDT on TRC-20 / ERC-20 / Solana; USDC on ERC-20 / Solana / Base.
  • Native assets: BTC, ETH, SOL, XRP, plus EVM L2 and BSC native tokens.
  • Per-line network choice — mix Tron, Ethereum, Base, and Solana payouts in one batch.
  • Reorg-aware crediting with per-chain confirmation thresholds before a payout reports complete.
06

Who runs payouts at this scale

The recipient list changes shape by business, but the operational problem is identical: many destinations, varying amounts, a recurring cadence, and zero tolerance for paying someone twice. Affiliate and partner programs settle commissions to a long tail of accounts on a fixed schedule. Trading and brokerage platforms push withdrawals and revenue shares to clients. Creator and marketplace platforms pay out earnings per cycle. Contractor and gig platforms run global payroll without local banking rails in every country.

For these teams the value isn't a flashy UI — it's that payout day stops being a manual, error-prone event. Stage the batch from your existing ledger, let the idempotency keys absorb every retry and restart, approve once, and reconcile against a clean per-recipient result. The same primitives back single payouts when you need to send to just one destination; mass payouts are the same guarantees applied to the whole list at once.