Products

Crypto payroll: pay staff and contractors in one approved run

Payroll is the payout run you cannot get wrong twice. The roster is mostly the same every cycle, the amounts are known in advance, and the cost of an error is a person who didn't get paid or got paid twice. halfin payroll payouts model a pay cycle as one idempotent batch: stage every person from your own records, give each line a deterministic idempotency key, have an operator approve the run, and reconcile against a clean per-person result. The mechanics are the mass-payout mechanics — this page is about applying them to a fixed roster you pay on a schedule.

01

Why payroll is harder than a one-off send

A single payout is easy to reason about: one destination, one amount, you watch it settle. Payroll is the opposite shape — dozens or hundreds of destinations, each with its own amount, repeated on a cadence, against a roster that drifts as people join and leave. The hard part isn't the transfers; it's keeping the run safe to interrupt. If your payroll script dies partway through, the question "who has already been paid this cycle?" has to have a definite answer, or someone gets paid twice and someone else gets skipped.

The expensive failure is the silent double-pay on a known recipient. A contractor's transfer times out ambiguously — the transaction may already be on-chain even though your client never got a response — and a naive re-run pushes a second payment to a person who already received this cycle's pay. Unlike an anonymous one-off, payroll recipients are recurring and named, so a duplicate is both a real loss and a relationship problem you hear about directly.

halfin removes the ambiguity by making each payroll line idempotent. Derive the key from the thing that uniquely identifies a payment in your books — the worker's account id joined with the pay period — and re-submitting the run returns the payout halfin already created instead of making a new one. The cycle becomes safe to re-run from the top after any crash, restart, or timeout, which is the one property a hand-rolled payroll loop never reliably has.

02

A pay cycle is one batch you stage, then approve

Model each pay period as a batch built from your own roster. Every line names a currency, an amount as a string, a destination address, and a deterministic idempotency key. Because the key is reproducible, re-running the same period reproduces the same keys, and halfin matches each one to the payout it already holds — so a retry converges on exactly one payment per person rather than a second round of pay.

Staging a run is not paying it. Each payout enters a pending-approval state and is released from the dashboard, so the batch your integration POSTs is a proposal a human still has to sign off. For payroll that separation is the whole point: your system can assemble the cycle unattended from HR or contractor data, and a finance operator reviews the total and the line count before any funds leave the balance. Nobody's pay goes out because a cron job fired at the wrong time.

Roster changes are absorbed line by line. A contractor who left has no line this cycle; a new hire gets a fresh line with a fresh key. If a line is rejected up front — a malformed address, an amount below a chain's dust threshold, an asset the destination can't receive — only that line fails and is reported back. You fix the offending rows and re-submit the whole period: the lines that already succeeded are no-ops, and only the corrected ones execute. One bad row never blocks the rest of the run.

03

What a payroll line carries

Every person in the run is described by the same small set of fields. Amounts are always strings, never floating-point numbers, so a contractor's pay is transported exactly with no binary-rounding drift between your ledger and the chain. Currency and network are chosen per line, so the same cycle can pay people on the rails they actually use — a contractor in USDT on Tron, a staff member in USDC on Base, a builder in native SOL.

FieldTypeNotes
currencystringAsset + network, e.g. USDT (TRC-20 / ERC-20 / Solana), USDC (ERC-20 / Solana / Base), BTC, ETH, SOL.
amountstringEach person's pay as a decimal string. Never a JS number — payroll math stays exact end to end.
destinationstringThe worker's address; validated for the chosen network before the line is accepted.
idempotency_keystringDeterministic per worker + pay period, e.g. account id joined with the cycle. Re-submitting returns the original payout.
04

Running a pay cycle from code

There is no dedicated payroll endpoint to learn. A pay run is a fan-out over the single-payout API, where the idempotency key makes the loop safe to interrupt and re-run. Build each key from your own records so the same person in the same period always maps to the same key, then wrap the whole loop in a retry — the second pass is correct by construction, not dangerous.

The example below walks a roster file and submits each worker's line with its own key built from the account id and the pay period. The only headers a request carries are `X-API-Key` and `Content-Type`; the idempotency key is a snake_case field in the request body, not a header. After the run is staged, an operator releases it from the dashboard before anything settles.

# Stage a pay cycle; each line is idempotent, so re-running after a crash is safe.
PERIOD=2026-06
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\": \"payroll-$PERIOD-$account\"
    }"
done < roster.csv
# Re-run the same period after a failure: keys collide, nobody is paid twice.
# Then a finance operator approves the staged run in the dashboard before funds move.
05

Funding the run and keeping the books straight

Payroll has to be paid from a balance that actually holds the right asset on the right chain. A USDT-on-Tron payroll line draws on a USDT-on-Tron balance; a USDC-on-Base line draws on USDC on Base. When your treasury is sitting in one asset but the cycle pays in another, balance conversion is the bridge — convert asset-to-asset before you stage the run so each line has funds behind it. Balance conversion is treasury rebalancing between crypto assets, not a fiat off-ramp, and it does not pay anyone on its own; it just makes sure the right asset is available when the run is approved.

Because every line carries a deterministic key tied to a worker and a period, the run reconciles cleanly against your own payroll ledger. Each person maps to exactly one payout per cycle, partial failures are reported per line, and the approval step gives finance a single point to confirm the total before release. Webhooks close the loop after approval: a `payout.completed` event tells you a worker's pay has settled, and `payout.failed` flags a line that needs attention — verify the HMAC signature on the event before you act on it.

  • Per-line network choice — pay contractors on USDT/Tron, staff on USDC/Base or Ethereum, builders on native SOL, in one run.
  • Operator approval before any funds leave the balance — the staged cycle is a proposal, not an instant transfer.
  • Deterministic keys keep the cycle idempotent across crashes, restarts, and re-runs — one payment per person.
  • Settlement is reorg-aware with per-chain confirmation thresholds, so a line reporting complete has actually settled.
  • `payout.completed` / `payout.failed` webhooks (HMAC-signed) drive your own payroll status without polling.
06

Recurring pay without saved cards or stored credentials

Payroll is recurring by nature, but halfin does not auto-charge or hold anyone's payment instrument. Each cycle is a fresh batch you assemble and approve — the recurrence lives in your scheduler, not in a stored mandate. That keeps the model honest: there is no standing authority to move funds, every run passes through a human approval, and the idempotency keys make the scheduled re-submission safe rather than risky.

The same primitives back a single payout when you only need to pay one person off-cycle — a correction, a final settlement, a one-off bonus — and the mass-payout flow when the whole roster goes out together. Payroll is simply the recurring, roster-shaped case of the same guarantees: stage from your records, approve once, reconcile against a per-person result, and let the keys absorb every retry along the way.