Use case

Affiliate payouts in crypto: idempotent runs, one approval, a clean record

The amount each affiliate is owed is the easy part — your tracker already knows it. The hard part is the run: turning an approved-earnings export into hundreds of on-chain payments without paying anyone twice when a worker restarts, without one bad address stalling the whole file, and without a human pushing money before someone signs off. halfin gives the affiliate payout run three properties a hand-rolled loop never has: every publisher line carries its own idempotency key so a retry is safe, the batch your platform submits is a proposal that a treasurer releases once, and every payout resolves to a record your finance team can reconcile against a real transaction hash.

01

The affiliate payout run, not the commission, is where the risk lives

Your tracking platform is the system of record for what each affiliate earned. It holds approved, held, and reversed leads, applies the deal terms, and produces a number per publisher for the period. None of that is where programs lose money. The loss happens downstream, when that number has to become an actual on-chain payment to a real wallet — and the export becomes a loop that fires transfers one at a time under payout-day time pressure.

That loop has a specific failure shape. It dies partway through — a dropped RPC connection, a rate limit on a busy node, a wallet that ran out of gas — and the question nobody can answer cleanly is which publishers already received funds. Re-running the whole file risks paying everyone before the crash a second time. Resuming by hand from the failure point risks skipping someone whose send was in flight when the process died. Both choices are made fast, by a person who wants the run finished, and both are the kind of mistake an affiliate either disputes loudly or quietly keeps.

The worst version is the ambiguous timeout. A send request times out without a clear answer — the transaction may already be on-chain even though your client never saw the confirmation — and a naive retry treats it as a fresh send and pushes a second payment. On-chain payments are final, so the duplicate surfaces only when treasury reconciliation comes up short. halfin removes the ambiguity at its root: every payout carries a caller-supplied idempotency key, and re-submitting the same key returns the original payout instead of creating a new one. The run becomes safe to retry from the top, which is the exact property an affiliate payout needs.

02

Derive the key from your ledger, and a retry can only converge

The idempotency key is the whole mechanism, so it is worth being precise about where it comes from. You do not generate a random key per request — a random key would make a retry create a duplicate, defeating the point. You derive the key deterministically from your own records: the affiliate's account id joined with the payout period. The same publisher in the same cycle therefore always produces the same key, on the first attempt and on every retry, so re-submitting the file reproduces the keys exactly and halfin matches each one to the payout it already created.

That determinism is what makes wrapping the whole run in a retry correct rather than dangerous. A second pass over the file converges on exactly one payout per publisher: lines that succeeded are no-ops, and only the missing ones execute. The loop below walks an approved-earnings export and submits each line with its derived key. There is no separate batch endpoint to learn — a mass affiliate payout is a fan-out over the single-payout API, and the keys are what make the fan-out safe to interrupt.

# Pay an affiliate run: one payout per publisher line, key derived from your ledger.
# Re-running the same export after a crash pays no one twice — keys collide.
while IFS=, read -r affiliate 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\": \"affiliate-2026-06-$affiliate\"
    }"
done < approved-earnings.csv
# A second pass converges on exactly one payout per affiliate.
03

Stage unattended, approve once: the run is a proposal until released

Payouts do not move funds the instant your platform calls 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 single design choice separates the two things an affiliate payout run needs to keep apart: the high-throughput programmatic path that stages thousands of publisher lines unattended, and the human control point where whoever owns the treasury signs off before anything leaves the balance.

In practice the run has four distinct steps, and they belong to different owners. Your integration stages the batch from the tracker's approved-earnings export — fully automated, no person in the loop. Partner-ops and finance review the staged batch in the dashboard, where they can catch a publisher whose deal changed mid-period or an amount that looks wrong before money moves. A treasurer releases it once. Then halfin settles each line on-chain and emits a signed event your platform consumes to mark the publisher paid. The programmatic path never pushes money on its own, and the human approval is one action over the whole batch rather than a per-line click-through.

StepWho owns itWhat it produces
Stage the batchYour integration (automated)One payout line per publisher, each with a key derived from your ledger.
ReviewPartner-ops / financeA human check of amounts and recipients before any funds move.
Approve and releaseTreasurer (one action)The whole batch released from the dashboard — the single irreversible step.
Settle and confirmhalfinOn-chain payouts plus a signed payout.completed event per line, each with a transaction hash.
04

Handle the rejected rows without stalling the good ones

Partial failure is treated as the normal case, not an exception that aborts the run. Some publisher lines get rejected up front: a malformed address a publisher pasted wrong, an amount below a chain's dust threshold, a currency the destination wallet cannot receive. Those are reported per-line, and the rest of the batch still goes through. There is no step where one bad row blocks two hundred good ones, and no spreadsheet column where someone hand-tracks paid versus not-paid.

The recovery loop is the same loop you already ran. You fix the rejected rows in your export — correct the address, adjust the amount, pick the right network — and re-submit the whole file. Because every line still carries its deterministic key, the publishers already paid are no-ops, and only the corrected rows execute. You never carry partial state between attempts or reason about which subset to re-send; you always re-run the complete export and let the keys absorb the difference.

  • A bad address, dust-threshold amount, or unsupported currency rejects only that line.
  • Rejections are reported per-line, so the rest of the batch settles normally.
  • Fix the rejected rows in the export and re-submit the whole file — paid lines are no-ops.
  • No manual paid/not-paid tracking and no partial-state bookkeeping between attempts.
05

Pay each publisher on the chain they actually use

A global affiliate base does not share one preferred rail, and forcing everyone onto a single network produces rejected payments and addresses that cannot receive what you sent. A publisher running traffic in one region may settle in USDT on Tron because the network fee is predictable; a US-facing affiliate may want USDC on Ethereum or Base for their own accounting; another may prefer Solana for fast, low-cost settlement. Each payout line picks its own currency and network, so a single affiliate run can span all of them at once.

Settlement is reorg-aware and uses per-chain confirmation thresholds, so a payout reported complete has actually settled to the depth that chain requires — not merely been broadcast. That distinction matters most at the tail of a large run, where you want each publisher's status to mean done rather than submitted and hopefully fine. Each line resolves to a transaction hash your support team can hand to a publisher who asks where their money is. When you collect deposits or top-ups in one asset but owe affiliates in another, balance conversion handles the treasury side so funding the run is not a separate manual desk operation.

  • 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 publishers in one run.
  • Reorg-aware crediting with per-chain confirmation thresholds before a line reports complete.
06

Reconcile against signed events, not a spreadsheet of checkboxes

The old affiliate payout process leaves no trustworthy record. A spreadsheet of paid checkboxes is not an audit trail, and a bank statement weeks later does not tie a line back to a specific publisher and period. halfin's run leaves a concrete chain of evidence per payout: an idempotency key derived from your ledger, a dashboard record, an HMAC-signed payout.completed webhook, and an on-chain transaction hash. When a publisher disputes a payment or finance closes the period, the answer is looked up rather than reconstructed from memory.

The reconciliation loop runs off the webhook, not off the API response your platform might have missed. When a line settles, halfin sends a payout.completed event; your platform verifies its HMAC signature before taking any action, then marks that publisher line settled in its own books. Because the key ties back to a specific account and period, a query like "we're short on the May run for account 4471" resolves against a deterministic key, an on-chain payout, and a signed completion event — not a forensic spreadsheet exercise. Reconciliation becomes matching signed events to publisher-and-period rows.

Responsibility stays cleanly divided, and halfin is deliberate about not overstating its role. Your platform owns the affiliate program: the tracker, the anti-fraud logic, the commission calculation, the payee list, and the wallet and counterparty screening before funds move. halfin receives the approved payout instructions, runs the rails, keeps the record, and returns signed status. halfin supports KYB onboarding when you become a merchant and applies AML awareness to the payment rail, but it is payment infrastructure — never a license, a certification, or your program's compliance approval. The full payout and webhook schemas live at docs.thehalfin.com.

  • Keep your tracker as the system of record for valid conversions and amounts owed.
  • Verify the payout.completed HMAC signature before marking a publisher line settled.
  • Tie every payout to a key, a dashboard record, a signed webhook, and a transaction hash.
  • Treat KYB and AML awareness as process — never as a license or certification claim.