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.
recipientWallet and for several destinations.
The rest of the flow does not change: estimate → create → prepare → sign and broadcast → confirm-broadcast.
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.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 oneMH_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)
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 sameroute 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:
route: the hops on chain and how long the route takes.
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
Passroute instead of hops and arrivalSeconds:
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 withrouteTemplateId in place of route:
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.
