Skip to main content
By default a transfer’s route is chosen for you: you pass hops and arrivalSeconds, and MultiHopper picks the wallets and spaces them out. With a custom route you choose them yourself. You pass route.hops, and each hop is either:
  • a wallet you name: { "wallet": "<address>", "waitSeconds": 120 };
  • a wallet drawn from the hop library: { "bundle": "cex", "waitSeconds": 300 }. The draw is random and happens when the transfer is created.
waitSeconds is how long the funds stay in that wallet before they move on.
A custom route works for one recipientWallet and for several destinations. The rest of the flow does not change: estimate → create → prepare → sign and broadcast → confirm-broadcast.
A custom route chooses which wallets the funds pass through. It does not make a transfer untraceable. Every hop is an ordinary on-chain transfer that anyone can follow. Library wallets are well-known public addresses, and the route’s hop list is public.

How a route runs

The first hop receives the funds as soon as the route starts. Each later hop receives them when the waits before it have passed. To one recipient, the route ends on the recipient: it is added as the last hop and is paid once every wait has passed. Paying several destinations, the route ends on your last hop, and the destinations are paid from there.
The route takes the sum of its waits (here 1,020 seconds), so you do not pass hops or arrivalSeconds. The schedule starts when you call /prepare. If you prepare later than the quote was made, every hop moves by the same amount, so no wait gets shorter. The times go on chain when you broadcast, so sign and broadcast promptly after /prepare: a hop whose time has already passed runs as soon as it can. On compliance-enabled networks, hops start once the route has passed its screen, which usually takes a few seconds. Hop wallets never sign anything: each one receives the route’s wrapped token for its wait, and the program moves it on.

Rules

Every rule is checked before anything is created. Every problem comes back at once, in one MH_018 error that names the offending route.hops[i]. A request that is malformed (an invalid address, a wait outside 20 seconds to 30 days, an unknown bundle, an extra field, more than 10 hops) is refused first with a BAD_REQUEST error naming the first such field. A middle hop can be any valid address, programs and program-derived addresses included. A library draw never repeats a wallet that is already on the route, and never picks the sender, the recipient or a destination.

1. Look at the library (optional)

The response lists the wallets, each with its label, category and canEndRoute, plus every bundle and how many wallets it has: You can also name a library wallet directly as { "wallet": "<its address>" }. The library is not served on devnet (MH_019): build devnet routes from your own wallets.

2. Estimate: a free dry run

Send the same route to /estimate. It checks every rule that does not need the sender’s, the recipient’s or the destinations’ addresses, and prices the route’s real hop count:
The response adds route: the hops on chain and how long the route takes.
Add destinationCount to price a route paying several destinations. Then the last hop is checked as the route’s end, and there is no recipient hop.

3. Create

Pass route instead of hops and arrivalSeconds:
The response’s route.hops shows the route as it will run, with the library draws filled in:
hops and arrivalSeconds on the transfer are the route’s own: here 4 and 1,020. Retries. Re-sending the same externalId with the same route returns the transfer you already have. A bundle hop matches any wallet of that bundle, so the request still matches after the draw. The same externalId with a different route returns MH_033.

4. Prepare, broadcast, confirm, track

These steps are the same as for any transfer: see the quickstart. /prepare restarts the schedule from that moment (see How a route runs). GET /transfers/{id} returns route.hops, and each hop’s executedAt and txSignature fill in as it runs.

Save and reuse a route

Save a route once, under a name, and reuse it with routeTemplateId in place of route:
The response’s id is the routeTemplateId. Each transfer copies the template’s hops and draws its own library wallets, so editing the template later changes only the transfers created after the edit. A template can be used with recipientWallet, with destinations, or with both; its usableFor says which. See Route templates.
The same externalId with the same routeTemplateId returns the transfer you already have, unless the template’s hops changed in between. Then the request no longer matches the transfer, and it returns MH_033.

Costs

A custom route is priced like any route with the same number of hops. On compliance-enabled networks every hop is screened when the route starts, and the screening fee is per wallet. Each hop also costs about 0.0019 SOL of token-account rent, which is not refunded. /estimate with your route quotes all of it.

Errors

For AI agents

An agent can build routes with the same calls, as tools of the MultiHopper MCP server: list_hop_library, then estimate_transfer with the route until it passes, then create_transfer with the same route (or create_route_template once and routeTemplateId after). Because MH_018 lists every problem at once, one estimate round shows the agent everything to fix. The server never holds a wallet key: the agent’s own wallet signs what prepare_transfer returns.