# Bulk payouts

> Pay up to 100 people from one wallet and token. POST /v1/payouts/plan checks and groups CSV or JSON rows; POST /v1/payouts/transfers builds same-chain transfer batches; other rows are normal quotes.

Source: https://docs.layerhop.com/guides/bulk-payouts

Bulk payouts pay a list of people (a team, contributors, prize winners) from one wallet and one token. Each row says who gets paid, how much, in which token and on which chain; recipients can be addresses, ENS names or `.sol` names. Rows that receive your own token on your own chain are plain transfers, sent in batches; every other row is a LayerHop route sized so the recipient gets the full amount. Every transaction is signed by your wallet; LayerHop never holds the funds.

In the app: [app.layerhop.com/payouts](https://app.layerhop.com/payouts) takes a pasted or uploaded CSV or a table, checks every address for look-alikes and other risks before you pay, runs the batches with retry, skip and resume, and downloads a receipt CSV with every transaction.

## What does the CSV look like?

```text
recipient,amount,token,chain,label
alice.eth,250,USDC,base,March design
0x3333333333333333333333333333333333333333,120.5,USDC,base
bob.sol,40,USDC,solana,Translations
carol.eth,0.05,ETH,arbitrum
```

- The header row is optional (columns may then be in any order: `recipient`/`address`, `amount`, `token`/`asset`, `chain`/`network`, `label`/`memo`). Without one the order is `recipient,amount,token,chain[,label]`.
- Commas, semicolons or tabs (pasted from a spreadsheet). Quote cells that contain the separator.
- `amount` is in human units of the token the recipient gets, with a dot for decimals and no thousands separators. It is converted to base units exactly; more decimals than the token has is an error, never rounded.
- At most 100 rows per payout. Blank lines and lines starting with `#` are skipped.

## How do I plan a payout with the API?

```bash
curl -X POST https://api.layerhop.com/v1/payouts/plan \
  -H "content-type: application/json" \
  -d '{"srcChain":"base","srcToken":"USDC","sender":"0xYOU","csv":"alice.eth,250,USDC,base\nbob.sol,40,USDC,solana"}'
```

Send `csv` (text) or `rows` (`[{ "recipient", "amount", "token", "chain", "label" }]`). Every row comes back, in order, with `ok` and either the resolved `recipient`, `token` (with a `usd` price when known), base-unit `amount` and `kind`, or an `error` (`INVALID_PARAMS`, `UNSUPPORTED_CHAIN`, `UNSUPPORTED_TOKEN`, `NOT_FOUND` for a name without an address, `PROVIDER_ERROR` when a name couldn't be resolved in time, `RATE_LIMITED` when name lookups are busy). A bad row never fails the request.

ENS names: on Ethereum a row pays the name's address. On another EVM chain it pays the name's address record for that chain (ENSIP-11), else the Ethereum address only when that is a plain wallet, or a contract that also exists at that address on the chain; a Safe or smart wallet that exists only on Ethereum makes the row `NOT_FOUND`, because funds sent there would be lost. Ask for an address instead.

Names cost several RPC calls each: a plan resolves at most 25 different names (rows with more ask for an address), answers within 20 seconds, and name lookups share a budget across all clients. Resolved names are reused for a minute.

The valid rows are grouped against your `srcChain` and `srcToken`:

- `transfers`: rows that receive your token on your chain, in batches of at most 20 (EVM) or 8 (Solana), with each batch's `total`. No LayerHop fee, only network gas.
- `routes`: every other row, grouped by destination chain. One quote and one signature per row; the normal swap fee applies.
- `totals`: what each destination token adds up to; `transferTotal`: what the transfers take from your wallet.

Check every resolved address before paying. The app runs LayerHop's recipient check on each one (look-alikes of addresses you used, poisoning transfers, contracts, unused addresses) and asks you to confirm the flagged ones.

## How do I send a transfer batch?

```bash
curl -X POST https://api.layerhop.com/v1/payouts/transfers \
  -H "content-type: application/json" \
  -d '{"chain":"base","token":"USDC","sender":"0xYOU","transfers":[{"recipient":"0x3333333333333333333333333333333333333333","amount":"120500000"}]}'
```

- EVM: `calls`, one per transfer (ERC-20 `transfer`, or a value transfer for the native coin). Send them as one EIP-5792 `wallet_sendCalls` where the wallet supports atomic batching, else one transaction each. `@layerhop/wagmi` does both: `sendWalletCalls(wagmiDriver(config), chainId, calls)` returns the transaction of every call, and on a sequential send that stops part-way throws `PartialSendError` with the ones already sent.
- Before asking the wallet, check every call against its row: `to` is the token contract (or the recipient, for the native coin), `data` is exactly `transfer(recipient, amount)` (or `0x`), `value` is `0` (or the amount). The app refuses to sign on any mismatch.
- Solana: `serializedTx`, one unsigned transaction with up to 8 transfers (each creates the recipient's token account if needed, then `TransferChecked`; SOL uses System `Transfer`). It has a fresh blockhash, so sign and send it within a minute. Classic SPL tokens only: a Token-2022 mint is `UNSUPPORTED_TOKEN` (pay those rows one by one or as routes), and an amount above the u64 maximum (18446744073709551615) is `INVALID_PARAMS`. SOL sent to a brand-new wallet must be at least the rent minimum (about 0.00089 SOL) or the transaction fails.

Never re-send a row that already has a transaction: if a batch stops part-way, build the next batch from the rows without one. The same goes for an EIP-5792 batch the wallet accepted: once `wallet_sendCalls` returned a batch id, its calls may land even if the status lookup fails or times out. `sendWalletCalls` then throws `BatchPendingError` (pass `onBatchSent` to record the id as soon as it exists); look the batch up later with `checkWalletCalls(driver, chainId, batchId)` (`success` with the transactions, `failed` when it reverted on chain and nothing moved, or still `pending`) instead of sending it again. The app records the batch id on the rows and offers **Check status**, never a retry.

## How do I pay a routed row?

Quote it like any swap, with the row's resolved address as `recipient` and `purpose=payout`:

```bash
curl "https://api.layerhop.com/v1/quote?srcChain=base&srcToken=USDC&amount=40400000&dstChain=solana&dstToken=USDC&sender=0xYOU&recipient=7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU&purpose=payout"
```

Quotes are for an input amount, so size it: start from the USD prices in the plan (target × destination price ÷ source price, plus 1%), then re-quote with `amount × target ÷ dstAmountMin` (plus 0.3%) until the quote's guaranteed minimum covers the row; two rounds are usually enough. Then build, sign and track it like any other order.
