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

# Split

> Create up to 50 swaps in one call and fund them all with one signed transaction on EVM or Solana

## Overview

Split lets partners create many independent swap orders in one API call. The orders share one `multiId`. Each order is priced and executed on its own, and you track them all with one status call.

You can fund the orders one by one, or fund the whole group at once with a **batch transaction** on Solana or EVM.

> [!NOTE]
>
> Split was previously called Multi-Swap. The API keeps the original names: endpoints live under `/exchanges/multi` and orders are grouped by `multiId`.

> [!NOTE]
>
> **Best For**: Platforms paying out to many recipients, batch airdrop tools, or any use case that needs several swaps from the same source token at once.

> [!WARNING]
>
> Split supports **standard (CEX) and private routing only** — DEX orders are not supported. You post each order with `from`/`to`/`amount`/`addressTo`. No `quoteId` is needed.

> [!NOTE]
>
> **Fixed rate** works per order: set `fixed: true` and include a `refundAddress`. **Reverse quotes** work too: add `amountType: "receive"` to a fixed order to pay the receiver an exact amount. See [Fixed Rate & Reverse Quote](https://docs.houdini.com/developer-hub/swap-flows/fixed-rate-swap.md).

## Key Features

> [card] Up to 50 Orders per Call
>
> Create up to 50 orders in one request, all linked by a shared `multiId`

> [card] Standard & Private Routing
>
> Choose standard (CEX) or private routing per order

> [card] One Status Call
>
> Track every order in the group with `GET /exchanges/multi/{multiId}`

> [card] Batch Funding on Solana and EVM
>
> Pay every deposit address with one signed transaction on Solana, Ethereum, Base or BNB Smart Chain

## How It Works

**1. Get Tokens**

Fetch tokens with `hasCex=true`. Add `hasBundler=true` if you want to fund the group with a batch transaction.

**2. Create a Split**

Call `POST /exchanges/multi` with an array of orders. To show rates first, call [Get split quotes](https://docs.houdini.com/api-reference/split/get-split-quotes.md) — it is optional.

**3. Wait for Deposit Addresses**

Orders are prepared in the background. Poll `GET /exchanges/multi/{multiId}` until every order has a `depositAddress`.

**4. Fund the Orders**

Fund all orders with one batch transaction (Solana or EVM), or send to each deposit address yourself.

**5. Monitor Status**

Keep polling `GET /exchanges/multi/{multiId}` until every order is finished.

### Choose How to Fund

| | Solana batch | EVM batch | Manual deposits |
| - | - | - | - |
| Chains | Solana | Ethereum, Base, BNB Smart Chain | Any |
| Source token | All native SOL, or all the same SPL token | All the chain's native coin, or all the same ERC-20 | Any mix |
| Calls | [Get batch transaction](https://docs.houdini.com/api-reference/bundler-sol/get-batch-transaction.md) | [Build batch transaction](https://docs.houdini.com/api-reference/bundler-evm/build-batch-transaction.md), then [Submit signed transaction](https://docs.houdini.com/api-reference/bundler-evm/submit-signed-transaction.md) | None |
| User actions | Sign the returned transaction(s) and broadcast them | Fund a smart account once, then sign one message | One transfer per order |

## Integration Guide

### Step 1: Get Tokens

Use token IDs (not symbols) when building your orders. Fetch and cache the token list from `/tokens` with `hasCex=true`. To fund the group with a batch transaction, also pass `hasBundler=true` — it returns only tokens the bundler can pay with.

> [tab] Bulk Fetch + Cache
>
> > [!WARNING]
> >
> > Cache the token list in your backend database. Never call `/tokens` on every user request — load on server startup or via a scheduled job and refresh every 24 hours.
>
> ```javascript JavaScript theme={null}
> async function fetchAllCexTokens() {
>   let page = 1;
>   let allTokens = [];
>
>   while (true) {
>     const response = await fetch(
>       `https://api-partner.houdiniswap.com/v2/tokens?hasCex=true&pageSize=20&page=${page}`,
>       {
>         headers: { 'Authorization': `${API_KEY}:${API_SECRET}` }
>       }
>     );
>
>     const { tokens, totalPages } = await response.json();
>     allTokens = allTokens.concat(tokens);
>
>     if (page >= totalPages) break;
>     page++;
>   }
>
>   return allTokens; // save to your DB
> }
> ```
>
> ```bash cURL theme={null}
> curl -X GET "https://api-partner.houdiniswap.com/v2/tokens?hasCex=true&pageSize=20&page=1" \
>   -H "Authorization: your_api_key:your_api_secret"
> ```

> [tab] Token Search
>
> ```javascript JavaScript theme={null}
> async function searchTokens(query) {
>   const params = new URLSearchParams({
>     term: query,
>     hasCex: 'true',
>     pageSize: '20',
>     page: '1'
>   });
>
>   const response = await fetch(
>     `https://api-partner.houdiniswap.com/v2/tokens?${params}`,
>     { headers: { 'Authorization': `${API_KEY}:${API_SECRET}` } }
>   );
>
>   const { tokens } = await response.json();
>   return tokens;
> }
> ```
>
> ```bash cURL theme={null}
> curl -X GET "https://api-partner.houdiniswap.com/v2/tokens?term=solana&hasCex=true&pageSize=20&page=1" \
>   -H "Authorization: your_api_key:your_api_secret"
> ```

> [tab] Batch-Ready Tokens
>
> ```javascript JavaScript theme={null}
> // Tokens that can fund a whole Split in one transaction, on one chain
> const params = new URLSearchParams({
>   hasCex: 'true',
>   hasBundler: 'true',
>   chain: 'base',
>   pageSize: '20',
>   page: '1'
> });
>
> const response = await fetch(
>   `https://api-partner.houdiniswap.com/v2/tokens?${params}`,
>   { headers: { 'Authorization': `${API_KEY}:${API_SECRET}` } }
> );
>
> const { tokens } = await response.json();
> ```
>
> ```bash cURL theme={null}
> curl -X GET "https://api-partner.houdiniswap.com/v2/tokens?hasCex=true&hasBundler=true&chain=base&pageSize=20&page=1" \
>   -H "Authorization: your_api_key:your_api_secret"
> ```

### Step 2: Create a Split

Call `POST /exchanges/multi` with an `orders` array (1 to 50 orders). Each order is independent — different token pairs, amounts, and destination addresses can share one batch. Set `anonymous: true` on any order to use private routing for that order.

> [!TIP]
>
> Planning to fund with a batch transaction? Every order must send the **same source token** on the same chain.

```javascript JavaScript theme={null}
const response = await fetch('https://api-partner.houdiniswap.com/v2/exchanges/multi', {
  method: 'POST',
  headers: {
    'Authorization': `${API_KEY}:${API_SECRET}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    orders: [
      {
        from: '6689b73ec90e45f3b3e51577',       // SOL token id
        to:   '6689b73ec90e45f3b3e51566',       // ETH token id
        amount: 10,
        addressTo: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
      },
      {
        from: '6689b73ec90e45f3b3e51577',       // SOL token id
        to:   '6689b73ec90e45f3b3e51558',       // USDC token id
        amount: 25,
        addressTo: '0xABcD1234abcd1234ABCD1234abcd1234ABCD1234',
        anonymous: true                          // private routing for this order
      },
      {
        from: '6689b73ec90e45f3b3e51577',       // SOL token id
        to:   '6689b73ec90e45f3b3e51577',       // SOL token id
        amount: 5,
        addressTo: 'RecipientSolanaAddressHere1111111111111111',
        anonymous: true,
        useXmr: true                            // force XMR as the private route
      }
    ]
  })
});

const result = await response.json();
console.log('Multi ID:', result.multiId);       // shared group identifier

result.orders.forEach((item, i) => {
  if (item.error) {
    console.error(`Order ${i} failed:`, item.error.message, item.error.code);
  } else {
    console.log(`Order ${i} ID:`, item.order.houdiniId);
    console.log(`  Send:`, item.order.inAmount, item.order.inSymbol);
  }
});
```

```bash cURL theme={null}
curl -X POST "https://api-partner.houdiniswap.com/v2/exchanges/multi" \
  -H "Authorization: your_api_key:your_api_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "from": "6689b73ec90e45f3b3e51577",
        "to": "6689b73ec90e45f3b3e51566",
        "amount": 10,
        "addressTo": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
      },
      {
        "from": "6689b73ec90e45f3b3e51577",
        "to": "6689b73ec90e45f3b3e51558",
        "amount": 25,
        "addressTo": "0xABcD1234abcd1234ABCD1234abcd1234ABCD1234",
        "anonymous": true
      }
    ]
  }'
```

### Create Split Response

```json theme={null}
{
  "multiId": "8ppABEC1HZGrSr1PGbUnCN",
  "orders": [
    {
      "order": {
        "houdiniId": "nkuYKXXrfzabgG4u3nZhpq",
        "multiId": "8ppABEC1HZGrSr1PGbUnCN",
        "created": "2026-09-24T09:45:09.594Z",
        "expires": "2026-09-24T10:15:09.594Z",
        "receiverAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
        "anonymous": false,
        "fixed": false,
        "status": -1,
        "statusLabel": "NEW",
        "displayStatus": "WAITING_FOR_DEPOSIT",
        "inAmount": 10,
        "inSymbol": "SOL",
        "outSymbol": "ETH"
      }
    },
    {
      "error": {
        "message": "Amount is below the minimum for this pair",
        "code": "SWAP_AMOUNT_IS_OUT_OF_BOUNDS"
      }
    }
  ]
}
```

**Key Response Fields:**

* `multiId`: Shared identifier for the group — use it for status and batch funding
* `orders[]`: One entry per order you sent, in the same order
* `orders[].order`: The created order (see [Order Lifecycle](https://docs.houdini.com/developer-hub/core-concepts/order-lifecycle.md))
* `orders[].error`: Why this order was not created. The rest of the group is unaffected
* `statusLabel`: Starts as `NEW`. The order is prepared in the background

> [!WARNING]
>
> Orders usually come back **without a `depositAddress`**. They are prepared in the background. Poll the Split status (Step 3) until every order has its `depositAddress` before you send funds.

If every order fails, the call returns an error instead of a `200`, and nothing is created.

### Order Fields Reference

| Field | Description |
| - | - |
| `from` | Token ID of the input token (24-char MongoDB ObjectId) |
| `to` | Token ID of the output token |
| `amount` | Amount to send. With `amountType: "receive"`, the amount the receiver gets |
| `amountType` | `send` (default) or `receive`. `receive` pays the receiver an exact amount and **requires `fixed: true`** |
| `addressTo` | Destination wallet address for output funds |
| `anonymous` | `true` to use private routing |
| `destinationTag` | Memo/tag for chains that require it (e.g. XRP, XLM) — max 64 chars |
| `useXmr` | `true` to force XMR as the private route |
| `walletInfo` | Additional wallet metadata — max 256 chars |
| `fixed` | `true` to lock the rate for this order. Requires `refundAddress` |
| `refundAddress` | Source-chain wallet for refunds. **Required when `fixed` is `true`** |
| `refundExtraId` | Memo/tag for `refundAddress` on memo-bearing chains (XRP, Stellar, TON) — max 64 chars |
| `intermediateRefundAddress` | Private orders only: where USDC goes if the order's on-chain hop fails. See [Private On-chain Hop](https://docs.houdini.com/developer-hub/swap-flows/private-onchain-hop.md) |
| `markup` | Your markup for this order, as a percentage (0–4). Omit it to use your account default |

You can also send these at the top level of the request, next to `orders`:

| Field | Description |
| - | - |
| `filters` | Provider filters applied to every order in the group |
| `intermediateRefundAddress` | Default `intermediateRefundAddress` for every order that does not send its own |

### Step 3: Wait for Deposit Addresses

Poll `GET /exchanges/multi/{multiId}` to get every order in the group at once. Before funding, wait until every order has a `depositAddress`. An order that failed during setup shows `FAILED`, never gets one, and is left out of the batch.

```javascript JavaScript theme={null}
async function getSplitStatus(multiId) {
  const response = await fetch(
    `https://api-partner.houdiniswap.com/v2/exchanges/multi/${multiId}`,
    {
      headers: { 'Authorization': `${API_KEY}:${API_SECRET}` }
    }
  );
  return response.json();
}

async function waitForDepositAddresses(multiId) {
  while (true) {
    const { orders } = await getSplitStatus(multiId);
    const notReady = orders.filter(o =>
      !o.depositAddress && ['INITIALIZING', 'NEW'].includes(o.statusLabel)
    );
    if (notReady.length === 0) return orders;
    await new Promise(resolve => setTimeout(resolve, 5_000));
  }
}
```

```bash cURL theme={null}
curl -X GET "https://api-partner.houdiniswap.com/v2/exchanges/multi/8ppABEC1HZGrSr1PGbUnCN" \
  -H "Authorization: your_api_key:your_api_secret"
```

### Split Status Response

```json theme={null}
{
  "multiId": "8ppABEC1HZGrSr1PGbUnCN",
  "orders": [
    {
      "houdiniId": "q2R66TDXHeHKb5kxEcAo9C",
      "multiId": "8ppABEC1HZGrSr1PGbUnCN",
      "status": 0,
      "statusLabel": "WAITING",
      "displayStatus": "WAITING_FOR_DEPOSIT",
      "inAmount": 0.35,
      "inSymbol": "SOL",
      "outAmount": 0.01481099,
      "outSymbol": "ETH",
      "depositAddress": "So1anaExampleDepositAddress11111111111111111",
      "receiverAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
      "anonymous": false,
      "eta": 1,
      "created": "2026-09-24T09:45:16.943Z",
      "modified": "2026-09-24T09:46:09.972Z",
      "expires": "2026-09-24T12:45:15.466Z"
    }
  ]
}
```

**Key Fields:**

* `orders[].depositAddress`: Where this order's funds go. Missing until the order is ready
* `orders[].inAmount`: The exact amount to send to this order. For fixed and reverse orders, read the final `inAmount` and `outAmount` here once the order has a `depositAddress`
* `orders[].expires`: Fund the order before this time
* `bundleStatus`, `txHash`, `failureReason`: EVM batch only — see "Track the batch" under Step 4

Orders older than 48 hours come back as a short entry with only `houdiniId` and `status: 8` (deleted).

### Step 4: Fund the Orders

Pick one way to fund the group. Each order needs exactly its `inAmount` of the source token at its `depositAddress`.

> [tab] Solana batch
>
> #### Get the transactions
>
> Call `GET /exchanges/multi/{multiId}/tx` with the wallet that will pay. The response has unsigned transactions that pay every deposit address. Sign them and broadcast them yourself. There is nothing to submit back to Houdini.
>
> | Query | Description |
> | - | - |
> | `sender` | **Required.** Solana wallet that signs and pays, including the network fee |
> | `txVersion` | `legacy` (default), `v0` or `v1`. Leave it out if you are not sure. See [Choosing a transaction format](https://docs.houdini.com/api-reference/bundler-sol/get-batch-transaction.md#choosing-a-transaction-format) |
> | `houdiniIds` | Optional. Comma-separated order IDs, to fund only some orders |
>
> One transaction pays up to:
>
> | `txVersion` | Native SOL | SPL token |
> | - | - | - |
> | `legacy`, `v0` | 20 orders | 10 orders |
> | `v1` | 60 orders | 10 orders |
>
> A larger group comes back as several entries in `transactions`. Sign and broadcast every one. Most wallets can sign them all with one approval (`signAllTransactions`).
>
> ```javascript JavaScript theme={null}
> import { Connection, Transaction } from '@solana/web3.js';
>
> const connection = new Connection('https://api.mainnet-beta.solana.com');
> const sender = wallet.publicKey.toBase58();
>
> // Call from your server — it needs your API key
> const response = await fetch(
>   `https://api-partner.houdiniswap.com/v2/exchanges/multi/${multiId}/tx?sender=${sender}`,
>   { headers: { 'Authorization': `${API_KEY}:${API_SECRET}` } }
> );
> const { chain, txVersion, transactions } = await response.json();
> // chain: "sol", txVersion: "legacy"
>
> // Decode every transaction (legacy format)
> const txs = transactions.map(batch =>
>   Transaction.from(Buffer.from(batch.txData.data, 'base64'))
> );
>
> // One approval in the wallet signs them all
> const signed = await wallet.signAllTransactions(txs);
>
> for (const tx of signed) {
>   const signature = await connection.sendRawTransaction(tx.serialize());
>   console.log('Sent:', signature);
> }
> ```
>
> ```bash cURL theme={null}
> curl -X GET "https://api-partner.houdiniswap.com/v2/exchanges/multi/8ppABEC1HZGrSr1PGbUnCN/tx?sender=8jZnXYnZB1MJQG6zBXyouzcycsaPTrZtPXPHDNifjYaC" \
>   -H "Authorization: your_api_key:your_api_secret"
> ```
>
> Signing on a server with a keypair instead of a wallet? Call `tx.sign(keypair)` on each transaction, then send it the same way. For `v0`, decode with `VersionedTransaction.deserialize(...)` instead.
>
> #### Solana Batch Response
>
> ```json theme={null}
> {
>   "multiId": "8ppABEC1HZGrSr1PGbUnCN",
>   "chain": "sol",
>   "txVersion": "legacy",
>   "transactions": [
>     {
>       "houdiniIds": ["nkuYKXXrfzabgG4u3nZhpq", "q2R66TDXHeHKb5kxEcAo9C"],
>       "txData": {
>         "data": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB..."
>       }
>     }
>   ]
> }
> ```
>
> * `chain`: Always `"sol"` on this endpoint
>
> * `txVersion`: The format you got back
>
> * `transactions[].houdiniIds`: The orders this transaction pays
>
> * `transactions[].txData.data`: The unsigned transaction, base64 encoded
>
> > [!WARNING]
> >
> > Each transaction carries a recent blockhash, so it is only valid for about a minute. Sign and broadcast it right away. If it expires, call the endpoint again — but pass `houdiniIds` for the unfunded orders only, so you never pay an order twice.
>
> Only orders still waiting for a deposit are included. The call is rejected while any order is still initializing — wait for Step 3, then try again.

> [tab] EVM batch
>
> The EVM batch pays every deposit address with one **ERC-4337 UserOperation**. The user funds a smart account once, signs one message, and Houdini submits the operation on-chain.
>
> > [!NOTE]
> >
> > Supported on **Ethereum, Base and BNB Smart Chain**. Every order must send the same token — all the chain's native coin (ETH, BNB), or all the same ERC-20. Up to 50 orders go in one UserOperation, so the user signs once.
>
> #### How the smart account works
>
> * Each sending wallet has its **own smart account**. Only that wallet can move funds out of it.
>
> * It has the same address every time for that wallet. It is created on first use, and the cost is included in the gas.
>
> * **Native coin source:** the smart account needs the swap amounts plus gas, in the native coin.
>
> * **ERC-20 source:** gas is paid in the same token, so the smart account needs only the token. The user still needs a little ETH or BNB to send the funding transfer.
>
> * Anything left over (for example unused gas) stays in the smart account. It counts toward the next batch from the same wallet with the same token, and `saCurrentBalance` shows it.
>
> > [!WARNING]
> >
> > The `sender` must be a regular wallet that can sign messages (an EOA such as MetaMask or Rabby). Smart-contract wallets like Safe cannot sign for the smart account and are rejected at submit.
>
> > [!NOTE]
> >
> > All API calls need your API key, so make them from your server. Only the wallet steps (send the funding transaction, sign the message) run where the user's wallet is.
>
> #### 4a. Build the batch
>
> Call `POST /exchanges/multi/{multiId}/tx/build` with the user's wallet as `sender`.
>
> ```bash cURL theme={null}
> curl -X POST "https://api-partner.houdiniswap.com/v2/exchanges/multi/mx8Qa1/tx/build" \
>   -H "Authorization: your_api_key:your_api_secret" \
>   -H "Content-Type: application/json" \
>   -d '{ "sender": "0xYourUserWalletAddress000000000000000000000" }'
> ```
>
> ```json Response (USDC on Base) theme={null}
> {
>   "multiId": "mx8Qa1",
>   "chain": "evm",
>   "depositNeeded": "500012687",
>   "saCurrentBalance": "0",
>   "transactions": [
>     {
>       "houdiniIds": ["hxK7mQp2", "hxK7mQp3"],
>       "txData": {
>         "userOpHash": "0xa52e838c3667a152f26a3e039abe5bd0274261ef64dfa32d45c3a4604537852b",
>         "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
>         "data": "0xa9059cbb000000000000000000000000067423f2da169b79497f1ae57863a1d1ee7af772000000000000000000000000000000000000000000000000000000001dcdb5a6",
>         "value": "0",
>         "chainId": 8453,
>         "tokenAmount": "500012687"
>       }
>     }
>   ]
> }
> ```
>
> | Field | Meaning |
> | - | - |
> | `depositNeeded` | How much the smart account still needs. `"0"` means it is funded. In wei for a native source, or the token's smallest unit for an ERC-20 source |
> | `saCurrentBalance` | What the smart account holds now, in the same unit |
> | `txData.userOpHash` | The message the user signs |
> | `txData.to` | Where the funding transaction goes. **The smart account for a native source, but the token contract for an ERC-20 source** |
> | `txData.value` | Native source: the native coin to send, in wei (equals `depositNeeded`). ERC-20 source: `"0"` |
> | `txData.data` | Native source: `"0x"`. ERC-20 source: a `transfer` to the smart account for the **full** amount the batch needs |
> | `txData.tokenAmount` | ERC-20 source: the token amount still missing. Native source: `"0"` |
> | `txData.chainId` | Chain to send the funding transaction on |
>
> There is one entry in `transactions` today. Still loop over it, and sign each entry in order.
>
> #### 4b. Fund the smart account
>
> Skip this step if `depositNeeded` is `"0"`.
>
> * **Native source:** send `value` to `to`.
>
> * **ERC-20 source:** send a token `transfer` of `tokenAmount` to the smart account. The smart account address is the recipient inside `data`.
>
> > [!WARNING]
> >
> > **Do not send `tokenAmount` to `to`** for an ERC-20 source. `to` is the token contract, and tokens sent there are lost.
> >
> > Forwarding `data` unchanged moves the **full** amount. That is only right when `saCurrentBalance` is `"0"`. If the smart account already holds some tokens, it over-funds.
>
> Wait for the funding transaction to confirm. Then **build again** and use the new response. For an ERC-20 source, the `userOpHash` from before funding cannot be submitted — only a build made after funding returns one that works. Sign only once `depositNeeded` is `"0"`.
>
> #### 4c. Sign and submit
>
> Sign each `userOpHash` with the same wallet you passed as `sender`. Sign the hash as **raw bytes**, not as text — signing the `"0x…"` string gives a different signature, and the submit is rejected.
>
> * viem — `walletClient.signMessage({ account, message: { raw: userOpHash } })`
> * ethers v6 — `signer.signMessage(getBytes(userOpHash))`
> * ethers v5 — `signer.signMessage(ethers.utils.arrayify(userOpHash))`
>
> Then call `POST /exchanges/multi/{multiId}/tx` with the signatures, in the same order as `transactions`.
>
> ```bash cURL theme={null}
> curl -X POST "https://api-partner.houdiniswap.com/v2/exchanges/multi/mx8Qa1/tx" \
>   -H "Authorization: your_api_key:your_api_secret" \
>   -H "Content-Type: application/json" \
>   -d '{ "signatures": ["0x5f1c...1b"] }'
> ```
>
> ```json Response theme={null}
> {
>   "userOpHashes": ["0x9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f"]
> }
> ```
>
> > [!WARNING]
> >
> > A build is valid for **10 minutes**. For an ERC-20 source, submit within a few minutes of the last build. If the submit says the transaction expired, build again (the funds already in the smart account are kept) and sign the new hash.
>
> #### Full example (viem)
>
> ```javascript JavaScript theme={null}
> import {
>   createPublicClient, createWalletClient, custom, http,
>   decodeFunctionData, encodeFunctionData, erc20Abi
> } from 'viem';
> import { base } from 'viem/chains';
>
> const walletClient = createWalletClient({ chain: base, transport: custom(window.ethereum) });
> const publicClient = createPublicClient({ chain: base, transport: http() });
> const [sender] = await walletClient.getAddresses();
>
> // `api` calls your server, which adds your API key and forwards to Houdini
> const build = () => api('POST', `/exchanges/multi/${multiId}/tx/build`, { sender });
>
> // Build again until the smart account shows as funded. The balance can take
> // a few seconds to show after the funding transaction confirms.
> async function buildUntilFunded(attempts = 10) {
>   for (let i = 0; i < attempts; i++) {
>     const result = await build();
>     if (result.depositNeeded === '0') return result;
>     await new Promise(resolve => setTimeout(resolve, 3_000));
>   }
>   throw new Error('Smart account is still short. Check depositNeeded before sending more.');
> }
>
> // 4a. Build
> let result = await build();
>
> // 4b. Fund the smart account, then build again
> if (result.depositNeeded !== '0') {
>   const { to, data, value, tokenAmount, chainId } = result.transactions[0].txData;
>   const isNative = data === '0x';
>
>   let hash;
>   if (isNative) {
>     hash = await walletClient.sendTransaction({ account: sender, chainId, to, value: BigInt(value) });
>   } else {
>     const [smartAccount] = decodeFunctionData({ abi: erc20Abi, data }).args;
>     hash = await walletClient.sendTransaction({
>       account: sender,
>       chainId,
>       to, // the token contract
>       data: encodeFunctionData({
>         abi: erc20Abi,
>         functionName: 'transfer',
>         args: [smartAccount, BigInt(tokenAmount)],
>       }),
>     });
>   }
>
>   await publicClient.waitForTransactionReceipt({ hash });
>   result = await buildUntilFunded();
> }
>
> // 4c. Sign each userOpHash as raw bytes, in order, then submit
> const signatures = [];
> for (const { txData } of result.transactions) {
>   signatures.push(
>     await walletClient.signMessage({ account: sender, message: { raw: txData.userOpHash } })
>   );
> }
>
> const { userOpHashes } = await api('POST', `/exchanges/multi/${multiId}/tx`, { signatures });
> ```
>
> #### 4d. Track the batch
>
> After submit, poll `GET /exchanges/multi/{multiId}`. For an EVM batch it has three extra fields:
>
> | Field | Meaning |
> | - | - |
> | `bundleStatus` | `pending` (built, not submitted) → `submitted` → `confirmed` or `failed`. `withdrawn` after a recovery |
> | `txHash` | On-chain transaction hash, once the batch is confirmed |
> | `failureReason` | Why the batch failed. Only set when `bundleStatus` is `failed` |
>
> ```json theme={null}
> {
>   "multiId": "mx8Qa1",
>   "bundleStatus": "confirmed",
>   "txHash": "0x4b7a9c2e1d3f5a6b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b",
>   "orders": [ ... ]
> }
> ```
>
> `confirmed` means the deposits were sent. Each order then moves on by itself, so keep polling until every order is finished.
>
> #### If the batch fails
>
> | Situation | What to do |
> | - | - |
> | `bundleStatus` is `failed`, and the orders were created **less than 30 minutes ago** | Call [Retry failed orders](https://docs.houdini.com/api-reference/bundler-evm/retry-failed-orders.md) (`POST /exchanges/multi/{multiId}/retry`). It returns fresh `txData` with the same shape as a build. Fund `depositNeeded` if it is not `"0"` (then build again), sign, and submit as in 4c |
> | Retry is no longer possible (over 30 minutes, orders expired or refunded) | Call [Recover stuck assets](https://docs.houdini.com/api-reference/bundler-evm/recover-stuck-assets.md) (`POST /exchanges/multi/recovery` with `{ "multiId": "..." }`). Sign each returned `userOpHash` and submit with `POST /exchanges/multi/{multiId}/tx`. No funding is needed |
> | An order was refunded after the batch confirmed, and it had no `refundAddress` | The refund can land in the smart account. Use Recover stuck assets to send it back to the wallet |
>
> Recovery sends everything in the smart account back to the `sender` wallet. The gas is taken from the withdrawn funds. `bundleStatus` becomes `withdrawn` once it confirms. Recovery is rejected while the batch can still be retried, and returns `404` when there is nothing to withdraw.
>
> #### EVM batch errors
>
> | HTTP | When |
> | - | - |
> | `404` | The Split was not found, there is nothing waiting to be submitted (not built yet, or already submitted), no failed batch to retry, or nothing to withdraw. The `message` says which |
> | `409` `ALREADY_SUBMITTED` | You built again after the batch was submitted. Check the status instead |
> | `422` `VALIDATION_FAILED` | Read `fields` for the input and its `message`: orders still initializing, mixed source tokens or chains, unsupported chain, more orders than one batch allows, an expired build, a signature from the wrong wallet, another wallet already building this Split, a retry outside the window, or the network rejected the operation |
> | `503` | The bundler is temporarily unavailable. Try again shortly |

> [tab] Manual deposits
>
> Send each order's exact `inAmount` of the source token to its own `depositAddress`, before its `expires` time. Any wallet or exchange works, and orders can use different source tokens.
>
> Each order is funded and tracked on its own, exactly like a [Standard Swap](https://docs.houdini.com/developer-hub/swap-flows/standard-swap.md).

### Step 5: Monitor Split Status

Keep polling `GET /exchanges/multi/{multiId}`. Orders in the same group move at different speeds, so track each one's `statusLabel` on its own.

```javascript JavaScript theme={null}
const FINAL = ['FINISHED', 'FAILED', 'EXPIRED', 'REFUNDED'];

const interval = setInterval(async () => {
  const { orders } = await getSplitStatus(multiId);

  orders.forEach(order => {
    console.log(`${order.houdiniId}: ${order.statusLabel}`);
    // statusLabel values:
    // INITIALIZING  — order being set up
    // NEW           — no deposit address yet
    // WAITING       — awaiting deposit
    // CONFIRMING    — deposit detected, awaiting confirmations
    // EXCHANGING    — swap in progress
    // ANONYMIZING   — private routing step (private orders only)
    // FINISHED      — complete, funds sent
    // EXPIRED       — deposit not received in time
    // FAILED        — swap failed
    // REFUNDED      — funds returned
  });

  const allDone = orders.every(o => FINAL.includes(o.statusLabel) || o.status === 8);
  if (allDone) clearInterval(interval);
}, 30_000);
```

> [!TIP]
>
> Poll every 30 seconds. For a full list of statuses and transitions, see the [Order Lifecycle](https://docs.houdini.com/developer-hub/core-concepts/order-lifecycle.md) guide.

## Best Practices

> [accordion] Handle Per-Order Errors
>
> `POST /exchanges/multi` can succeed for some orders and fail for others. Check each item in `orders[]` for an `error` field. Handle failed orders on their own — the rest of the group still runs.

> [accordion] Fund Only When Every Order Is Ready
>
> Orders get their `depositAddress` in the background. Both batch endpoints refuse to build while any order is still initializing. Wait until every order shows `WAITING` with a `depositAddress`.

> [accordion] Never Pay an Order Twice
>
> An order stays `WAITING` until its deposit is detected, so a second batch call can include orders you just paid. On Solana, pass `houdiniIds` for the unfunded orders only. On EVM, a batch can only be submitted once — after submit, track `bundleStatus` instead of building again.

> [accordion] Keep the Same Wallet on EVM
>
> The wallet you pass as `sender` owns the smart account and must sign. While a build is live (10 minutes), building with a different wallet is rejected. Check `saCurrentBalance` before asking the user to fund — a previous batch may have left funds there.

> [accordion] Security
>
> Never expose API keys in frontend code. Validate all `addressTo` values before submission. Store `multiId` and each `houdiniId` for audit trails and support lookups.

## Common Issues

> [accordion] Some Orders Return Errors
>
> **Cause**: Individual orders can fail (unsupported pair, amount out of range, invalid address) while the rest of the group succeeds.
>
> **Solution**: Check each `orders[].error`. Create the failed orders again in a new Split with corrected values.

> [accordion] Batch Call Says Orders Are Still Initializing
>
> **Cause**: Some orders do not have a deposit address yet.
>
> **Solution**: Poll `GET /exchanges/multi/{multiId}` every 5–10 seconds until every order has a `depositAddress`, then build again.

> [accordion] Batch Not Available for My Tokens
>
> **Cause**: A batch needs every order to send the same token on one supported chain (Solana, Ethereum, Base or BNB Smart Chain), and the token must support the bundler.
>
> **Solution**: Pick tokens with `hasBundler=true`, or fund the orders manually.

> [accordion] EVM Submit Rejected: Signature Does Not Match
>
> **Cause**: The hash was signed as text instead of raw bytes, or by a different wallet than `sender`.
>
> **Solution**: Sign with `message: { raw: userOpHash }` (viem) or `getBytes(userOpHash)` (ethers), from the `sender` wallet.

> [accordion] EVM Batch Failed Right After an ERC-20 Deposit
>
> **Cause**: The signed `userOpHash` came from the build made before funding. For ERC-20 sources that hash cannot be submitted.
>
> **Solution**: Call [Retry failed orders](https://docs.houdini.com/api-reference/bundler-evm/retry-failed-orders.md) within 30 minutes, then sign and submit the new hash. Next time, build again after the funding transaction confirms.

> [accordion] depositNeeded Is Still Above 0 After Funding
>
> **Cause**: The balance can take a few seconds to show. Or the network fee went up since the build.
>
> **Solution**: Build again after a few seconds. If it stays above `"0"`, send only the new `depositNeeded` (native) or `tokenAmount` (ERC-20).

> [accordion] Order Expired Before Deposit
>
> **Cause**: The deposit window passed before funds were sent.
>
> **Solution**: Create the expired orders again in a new Split, and fund them right after their deposit addresses are ready. If an EVM batch had already funded the smart account, use Recover stuck assets to get the funds back.

> [accordion] Private Order Taking Longer
>
> **Cause**: Private routing adds an `ANONYMIZING` step.
>
> **Solution**: This is expected. Track `statusLabel` per order — private orders usually finish in 15–45 minutes.

## Next Steps

> [card] [Standard Swap](https://docs.houdini.com/developer-hub/swap-flows/standard-swap.md)
>
> Single-order CEX swaps

> [card] [Private Swap](https://docs.houdini.com/developer-hub/swap-flows/private-swap.md)
>
> Multi-hop private swaps

> [card] [Order Lifecycle](https://docs.houdini.com/developer-hub/core-concepts/order-lifecycle.md)
>
> Understand all order statuses

> [card] [Error Handling](https://docs.houdini.com/developer-hub/troubleshooting/errors.md)
>
> Handle errors and edge cases


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