> ## Documentation Index
> Fetch the complete documentation index at: https://dev-docs.multihopper.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom routes

> Choose every hop of a route: your own wallets, or wallets drawn from the hop library

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](https://dev-docs.multihopper.com/api-reference/hop-library.md):** `{ "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.

```json theme={null}
"route": {
  "hops": [
    { "bundle": "cex", "waitSeconds": 120 },
    { "wallet": "DQTf1yf1YM4B48PqJyQ53SEWGDM6ib4YBV7P3wm2MCxj", "waitSeconds": 300 },
    { "bundle": "kol", "waitSeconds": 600 }
  ]
}
```

A custom route works for one `recipientWallet` and for [several `destinations`](https://dev-docs.multihopper.com/guides/multi-destination).
The rest of the flow does not change: estimate → create → prepare → sign and broadcast → confirm-broadcast.

> [!WARNING]
>
> 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.

```
route.hops:  [ A wait 120 ]  →  [ B wait 300 ]  →  [ C wait 600 ]  →  recipient
time (s):        0                 120               420               1020
```

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.

| Rule | |
| - | - |
| Number of hops | 2–9 to one recipient (the recipient makes 3–10 on chain); 3–10 when paying several destinations |
| `waitSeconds` | a whole number from 20 to 2,592,000 (30 days) |
| Whole route | the waits add up to at most 30 days |
| Never a hop | the sender (`sourceOwner`), the recipient, a destination, a protocol account (system and token programs, MultiHopper's programs and accounts), or a wallet already on the route |
| Last hop, several destinations | a regular wallet: not a program-derived address or a program. A `contracts` bundle cannot be last |
| Recipient | a regular wallet, because the route ends on it |
| Not combined with | `hops` or `arrivalSeconds`: the route sets both |

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)

```bash theme={null}
curl https://multihopper.com/api/v1/hop-library?bundle=cex \
  -H "x-api-key: $MH_KEY"
```

The response lists the wallets, each with its `label`, `category` and `canEndRoute`, plus every
bundle and how many wallets it has:

| Bundle | Draws from |
| - | - |
| `cex` | centralised-exchange wallets |
| `dex` | DEX accounts |
| `exchanges` | `cex` and `dex` |
| `kol` | KOL wallets |
| `contracts` | smart contracts (middle hops only) |
| `mix` | every category |

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:

```bash theme={null}
curl -X POST https://multihopper.com/api/v1/transfers/estimate \
  -H "x-api-key: $MH_KEY" -H "Content-Type: application/json" \
  -d '{ "tokenMint": "So11111111111111111111111111111111111111112",
        "amountRaw": "1000000000", "tokenDecimals": 9,
        "route": { "hops": [
          { "bundle": "cex", "waitSeconds": 120 },
          { "wallet": "DQTf1yf1YM4B48PqJyQ53SEWGDM6ib4YBV7P3wm2MCxj", "waitSeconds": 300 },
          { "bundle": "kol", "waitSeconds": 600 } ] } }'
```

The response adds `route`: the hops on chain and how long the route takes.

```json theme={null}
"route": { "hopCount": 4, "arrivalSeconds": 1020 }
```

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`:

```bash theme={null}
curl -X POST https://multihopper.com/api/v1/transfers \
  -H "x-api-key: $MH_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "tokenMint": "So11111111111111111111111111111111111111112",
        "amountRaw": "1000000000", "amountTokens": "1", "tokenDecimals": 9,
        "sourceOwner": "<your wallet>",
        "recipientWallet": "7pBqizvE8vQZ29F49NWSu9cho3s7M9SqJyNN1UoQyQ2u",
        "externalId": "order_12345",
        "route": { "hops": [
          { "bundle": "cex", "waitSeconds": 120 },
          { "wallet": "DQTf1yf1YM4B48PqJyQ53SEWGDM6ib4YBV7P3wm2MCxj", "waitSeconds": 300 },
          { "bundle": "kol", "waitSeconds": 600 } ] } }'
```

The response's `route.hops` shows the route as it will run, with the library draws filled in:

```json theme={null}
"route": {
  "hops": [
    { "index": 0, "address": "64Cu…FErF", "label": "OKX Hot Wallet 10", "category": "cex", "scheduledAt": "2026-10-21T09:00:00.000Z", "executedAt": null, "txSignature": null },
    { "index": 1, "address": "DQTf…MCxj", "label": null, "category": null, "scheduledAt": "2026-10-21T09:02:00.000Z", "executedAt": null, "txSignature": null },
    { "index": 2, "address": "…", "label": "…", "category": "kol", "scheduledAt": "2026-10-21T09:07:00.000Z", "executedAt": null, "txSignature": null },
    { "index": 3, "address": "7pBq…QyQ2u", "label": null, "category": null, "scheduledAt": "2026-10-21T09:17:00.000Z", "executedAt": null, "txSignature": null }
  ]
}
```

`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](https://dev-docs.multihopper.com/quickstart.md). `/prepare` restarts the
schedule from that moment (see [How a route runs](#how-a-route-runs)).
[`GET /transfers/{id}`](https://dev-docs.multihopper.com/api-reference/transfers/get) 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`:

```bash theme={null}
curl -X POST https://multihopper.com/api/v1/route-templates \
  -H "x-api-key: $MH_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "name": "exchange-hop", "route": { "hops": [
          { "bundle": "cex", "waitSeconds": 120 },
          { "wallet": "DQTf1yf1YM4B48PqJyQ53SEWGDM6ib4YBV7P3wm2MCxj", "waitSeconds": 300 },
          { "bundle": "kol", "waitSeconds": 600 } ] } }'
```

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](https://dev-docs.multihopper.com/api-reference/route-templates.md).

> [!NOTE]
>
> 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

| Code | When |
| - | - |
| `BAD_REQUEST` | `route` is malformed: an invalid address, a wait out of range, an unknown bundle, an extra field. Names the first such field. |
| `MH_018` | The route breaks a rule. The message lists every problem, each naming its `route.hops[i]`. |
| `MH_019` | A `bundle` hop, or `GET /hop-library`, where the library is not served (devnet). |
| `MH_020` | A bundle has no wallet left that the route can use, for example when every one is already on the route. |
| `MH_021` | `routeTemplateId` names no template of your integration. |
| `MH_033` | The `externalId` exists with a different route. |

```json theme={null}
{ "error": { "code": "MH_018",
  "message": "Invalid custom route — route.hops[1]: DQTf…MCxj is the sender (sourceOwner); it cannot also be a hop; route.hops[2]: 7pBq…QyQ2u is the recipient; the route already ends on it",
  "requestId": "…" } }
```

## For AI agents

An agent can build routes with the same calls, as tools of the [MultiHopper MCP server](https://dev-docs.multihopper.com/guides/mcp-server.md):
`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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.