Skip to main content
Available on devnet. Production support is rolling out; until then, production requires recipientWallet and ignores destinations. Send exactly one of the two.
One transfer can pay up to 20 wallets. You pass destinations instead of recipientWallet, and the route pays each destination its share after the last hop. It costs noticeably less than one route per recipient. Most of a route’s cost is per hop, not per destination, so an extra destination adds only its payout transactions and a small, refundable step account.

How it differs from a single recipient

No destination is written into the route’s hop list. Each one is paid from an on-chain commitment after the route finishes.
This is not an anonymity guarantee. Payouts are ordinary on-chain transfers, and anyone analysing the chain can observe them. What changes is that the destinations never appear in the route’s published hop list.

1. Estimate

Pass destinationCount so the SOL estimate includes the payout accounts:
sol.breakdown.dispersionRentLamports is the extra rent. It comes back when the step accounts close.

2. Create

Each destination’s amountRaw is its share of the transfer’s amountRaw, and the shares must sum to it exactly. Fees come off the total once, and each destination receives its proportional share of what’s left (the hop amount):
The response lists what each destination will receive. Their amounts sum exactly to recipientReceivesRaw:
Rules, all checked before anything is created:
  • exactly one of recipientWallet or destinations;
  • shares sum to amountRaw; wallets are distinct; at most 20;
  • SOL: each destination must receive at least the rent-exempt minimum after fees, because a payout creates the destination account (MH_017);
  • SPL: needs a program version with SPL payout support on the cluster you’re calling, otherwise MH_017.

3. Prepare and broadcast

/prepare returns one extra group, dispersionInitTxs. It sets up the payout orchestrator, one committed step per destination, and the route’s exit binding. Sign each tx with sourceOwner and broadcast in order, after routeInitTxs:
resume.dispersion shows how much of the binding is already on chain, and resumed calls omit anything that has landed.

4. Confirm

Pass the new signatures as dispersionInitSignatures:
confirm-broadcast refuses to start a multi-destination transfer until the whole binding is on chain, and returns MH_041 with what’s missing. That’s deliberate: the route’s last hop is a decoy, so a route that ran without its binding would leave the funds parked. Broadcast whatever /prepare still returns, then confirm again.

5. Track payouts

After the last hop, the keeper pays each destination and records the payout signature. GET /transfers/{id} shows each destination’s status (pending → paid), payoutSignature and paidAt. The transfer stays active until every destination is paid, then becomes completed, and transfer.completed fires. For webhooks, completed means everyone was paid.