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.
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 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?#
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 isrecipient,amount,token,chain[,label]. - Commas, semicolons or tabs (pasted from a spreadsheet). Quote cells that contain the separator.
amountis 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?#
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'stotal. 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?#
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-20transfer, or a value transfer for the native coin). Send them as one EIP-5792wallet_sendCallswhere the wallet supports atomic batching, else one transaction each.@layerhop/wagmidoes both:sendWalletCalls(wagmiDriver(config), chainId, calls)returns the transaction of every call, and on a sequential send that stops part-way throwsPartialSendErrorwith the ones already sent. - Before asking the wallet, check every call against its row:
tois the token contract (or the recipient, for the native coin),datais exactlytransfer(recipient, amount)(or0x),valueis0(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, thenTransferChecked; SOL uses SystemTransfer). It has a fresh blockhash, so sign and send it within a minute. Classic SPL tokens only: a Token-2022 mint isUNSUPPORTED_TOKEN(pay those rows one by one or as routes), and an amount above the u64 maximum (18446744073709551615) isINVALID_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:
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.
Last updated