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

# Bridge all

# Bridge API — Full Reference (Alpha Preview)

> **Alpha Preview.** Endpoints, response shape, contract addresses, and supported
> routes may change without notice. Don't depend on this API for
> production-critical flows until it leaves alpha.

This is a single-file mirror of the Mobula Bridge API reference. It bundles the
three endpoint pages (`/quote`, `/status/:id` + `/status/:id/wait`, `/routes`)
into one markdown blob suitable for copy-paste or LLM context. Source of truth
remains the per-endpoint pages.

Base URL: `https://api.mobula.io/api/2/bridge` (production) or
`https://demo-api.mobula.io/api/2/bridge` (rate-limited, no key required).

The integration loop is always: `GET /quote` → sign the returned `deposit` →
`GET /status/{intentId}/wait` until terminal.

***

## Bridge Quote (`GET /api/2/bridge/quote`)

Returns a ready-to-sign deposit transaction plus an `intentId` you'll use to
poll status. The Mobula solver detects the deposit (flash blocks on Base,
gRPC on Solana) and fills on the destination chain.

Typical end-to-end latency: \~500 ms for Base ↔ Solana, \~1–3 s elsewhere.

### Query parameters

| Name                 | Required          | Notes                                                                                                                                          |
| -------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `originChainId`      | yes               | One of the supported chain IDs (see Bridge Routes section below).                                                                              |
| `destinationChainId` | yes               | Same set. When equal to `originChainId`, the endpoint short-circuits to the Swap API — see Same-chain quotes.                                  |
| `walletAddress`      | yes               | **Destination recipient.** Format-validated against the destination chain: EVM regex for `evm:*` and `hl:mainnet`, Base58 for `solana:solana`. |
| `amount`             | yes (cross-chain) | Decimal human units (`"0.05"`, not wei). Must be finite, positive, ≤ `1e15`. Not required when source and destination are the same chain.      |
| `originToken`        | no                | Omit, or pass `0x0000…0000` / `0xeeee…eeee` for the native token. EVM addresses are checksummed server-side.                                   |
| `destinationToken`   | no                | Same rules. When omitted on a Solana destination, the API substitutes wSOL (`So11111111111111111111111111111111111111112`).                    |
| `senderAddress`      | conditional       | **Required** for Solana SPL bridges (the controller needs it to build the ATA + SPL transfer + memo). Optional otherwise.                      |
| `slippage`           | no                | Percent. Default `1`, valid range `0` to `50`.                                                                                                 |

Validation errors come back as `{ "error": "..." }` with HTTP 200 — always
check the `error` key before reading `data`.

### Response

```json theme={null}
{
  "data": {
    "intentId": "a3b4bav-e34523c-324",
    "estimatedAmountOut": "0.68421052",
    "estimatedAmountOutUsd": "1.99",
    "fees": {
      "bridgeFeeBps": 0,
      "gasFeeUsd": "0.10",
      "totalFeeUsd": "0.25"
    },
    "estimatedTimeMs": 1000,
    "maxTradeUsd": 10000,
    "steps": [ /* optional, see below */ ],
    "deposit": { /* one of evm | solana | hl */ }
  }
}
```

* `intentId` is the user-facing handle in the format `xxxxxxx-xxxxxxx-xxx`
  (\~68 bits of entropy). Pass it to `GET /status/:id` or `/status/:id/wait`.
  There is also an on-chain `bytes32` intent ID emitted by `MobulaBridge` on
  EVM deposits — both resolve in `/status/:id`, so use whichever you have.
* `steps` is present only when the deposit needs more than one transaction.
  Today that means EVM ERC-20 origins: `[approve, bridgeToken | swapAndBridge]`.
  Native EVM bridges and Solana bridges omit `steps`.
* `gasFeeUsd` is a placeholder (`"0.10"`). The real fill gas is absorbed by the
  solver; the user only pays origin-chain gas to broadcast the deposit.
* `maxTradeUsd` is `$10,000`. Amounts above that return
  `"Amount $X exceeds maximum trade of $10000"`.

### `deposit` shapes

The shape depends on `originChainId`. Sign and broadcast whichever one is
present.

#### EVM origin (`deposit.evm`)

```json theme={null}
{
  "to": "0x867A784039D4842A32Ddd1277729Ad1373301458",
  "data": "0x...",
  "value": "50000000000000000",
  "chainId": 8453,
  "approvalAddress": "0x...",
  "approvalToken": "0x...",
  "approvalAmount": "115792...255"
}
```

Three code paths:

* **Native ETH/BNB/POL** — single `bridge()` call on `MobulaBridge`. `value`
  is the raw amount in wei. No `steps`.
* **Direct-bridge tokens** (USDC and USDT on each EVM chain) — `approve` step
  to `MobulaBridge`, then `bridgeToken()`. `value` is `"0"`.
* **Any other ERC-20** — `approve` step to `SwapBridgeHelper`, then
  `swapAndBridge()` — atomic swap to native + bridge in one TX. The embedded
  swap calldata is validated server-side to start with the
  `MobulaRouter.executeRoute` selector (`0xa564dfa4`); if it doesn't, the
  quote returns `"Swap quote failed: invalid calldata selector"`.

`MobulaBridge` addresses per chain:

| Chain                  | Bridge contract                              |
| ---------------------- | -------------------------------------------- |
| `evm:8453` (Base)      | `0x867A784039D4842A32Ddd1277729Ad1373301458` |
| `evm:56` (BSC)         | `0x7C57e0c67dE10cF1697326795326f5c8b073072c` |
| `evm:42161` (Arbitrum) | `0x7A0B2640E20a2e21c00E12925BEBD4a791082826` |
| `evm:137` (Polygon)    | `0xA28B4FFA97E475a1c584B388351092f4554C0DCC` |

Approval handling: `approvalAmount` is always `MAX_UINT256`, so a single
approve per (token, spender) is enough forever. Skip the `approve` step only
if the current on-chain allowance already covers `amount`.

#### Solana origin (`deposit.solana`)

Two shapes depending on token:

* **Native SOL** — `{ to, amount, memo }`. Build a `SystemProgram.transfer`
  for `amount` lamports to `to` (the solver address), then add a memo
  instruction (program `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`) whose
  data is the `memo` string. The memo is a JSON blob the Solana listener
  parses to recover `intentId`, `destinationChainId`, `recipient`,
  `destinationToken`, and `minAmountOut`.
* **SPL** — `{ type: "spl-transfer", serializedTx }`. The controller has
  already built the full versioned transaction (SPL transfer + ATA creation
  if missing + memo). Just `VersionedTransaction.deserialize`, sign, and
  send.

#### HyperLiquid origin (`deposit.hl`)

```json theme={null}
{
  "type": "spotSend",
  "to": "0x74CfC17edF89aD6134c04c446c3Be6dD288F0B8d",
  "token": "USDH:0x54e00a5988577cb0b0c9ab0cb6ef7f4b",
  "amount": "1.0"
}
```

Submit a `spotSend` action to the solver L1 address using your HL signer.

### Same-chain quotes

When `originChainId === destinationChainId`, the controller short-circuits
into a swap-wrapper. The response is the **raw Swap API response**, not the
bridge shape above (no `intentId`, no `deposit.evm/solana/hl`). Approval
amounts in the response are overridden to `MAX_UINT256` server-side.

Branch on the response: `data.deposit` present → bridge flow; otherwise →
swap flow.

### Side effects

Every `/quote` call upserts a row into `misc.bridge_intent_predictions` keyed
by `(sender, originChainId, destinationChainId)`. The destination-chain
listener reads it back to resolve `destinationToken`, `recipient`, and
`slippage` when the deposit lands. You do **not** need to encode those into
the EVM deposit TX — they're carried server-side.

***

## Bridge Status (`GET /api/2/bridge/status/{id}`) and Wait (`GET /api/2/bridge/status/{id}/wait`)

Two endpoints share this section:

* `GET /api/2/bridge/status/{id}` — returns the current row immediately.
* `GET /api/2/bridge/status/{id}/wait` — long-polls server-side until the
  intent reaches a terminal state (or the timeout elapses).

### Path parameter

`id` accepts **any** of these — pass whichever you have:

* `intentId` from `/quote` (format `xxxxxxx-xxxxxxx-xxx`).
* The on-chain `bytes32` intent ID (EVM only, emitted by `MobulaBridge`).
* The deposit TX hash.
* The fill TX hash.

If nothing matches, the response is **not** an error — it's:

```json theme={null}
{ "data": { "id": "...", "status": "pending", "message": "Intent not found — deposit may still be processing" } }
```

This is the expected state right after the deposit is broadcast but before
the chain listener has indexed it. Keep polling.

### Response

```json theme={null}
{
  "data": {
    "intentId": "a3b4bav-e34523c-324",
    "status": "filled",
    "originChainId": "evm:8453",
    "destinationChainId": "solana:solana",
    "originToken": "0x0000000000000000000000000000000000000000",
    "destinationToken": "So11111111111111111111111111111111111111112",
    "sender": "0x...",
    "recipient": "...",
    "amountIn": "0.05",
    "amountInUsd": 167.42,
    "amountOut": "0.68421052",
    "amountOutUsd": 165.10,
    "depositTxHash": "0x...",
    "fillTxHash": "5xL...",
    "settleTxHash": null,
    "latencyMs": 487,
    "timestamps": {
      "depositDetected": "2026-05-23T12:00:01.123Z",
      "fillSent": "2026-05-23T12:00:01.500Z",
      "fillConfirmed": "2026-05-23T12:00:01.610Z",
      "settled": null
    },
    "createdAt": "2026-05-23T12:00:00.000Z"
  }
}
```

`latencyMs` is the deposit-detected → fill-confirmed delta. `settleTxHash` /
`timestamps.settled` populate only once the solver has been reimbursed on
the origin chain — that step is async and can lag the user-visible fill.

### Status lifecycle

| Status      | Meaning                                             | Terminal?            |
| ----------- | --------------------------------------------------- | -------------------- |
| `pending`   | Deposit not yet detected (or row not yet written).  | No                   |
| `deposited` | Origin-chain listener saw the deposit; fill queued. | No                   |
| `filling`   | Fill TX broadcast on destination chain.             | No                   |
| `filled`    | Fill confirmed — user has received funds.           | **Yes** (happy path) |
| `settled`   | Solver reimbursed on origin chain.                  | Yes                  |
| `retrying`  | Fill failed; up to 3 attempts with backoff.         | No                   |
| `refunded`  | All retries exhausted; user refunded on origin.     | Yes                  |
| `failed`    | Refund also failed — manual intervention.           | Yes                  |

Stop polling once you see a terminal status.

### `GET /status/{id}/wait`

Long-poll variant. Blocks server-side until the intent reaches `filled`,
`settled`, or `failed`, then returns the same shape as `/status/{id}`. If
the window elapses first, you get the **current** row (still `pending`,
`deposited`, `filling`, or `retrying`) and should call again.

#### Query parameter

| Name      | Notes                                             |
| --------- | ------------------------------------------------- |
| `timeout` | Milliseconds. Default `30000`, capped at `60000`. |

#### How it actually waits

The controller registers a Postgres `LISTEN filled:<intentId>` and races it
against two timers:

* The `timeout` ceiling (≤ 60 s).
* A 3 s fallback timer that, if the pg notification hasn't fired by then,
  starts polling the DB once a second. This survives a missed `NOTIFY` so
  you'll always get a terminal status as long as the row eventually flips.

In practice, fills are delivered with the latency of the destination-chain
listener — typically a few hundred ms. The 30 s default is just an upper
bound; you almost always return earlier.

#### Recommended client loop

```typescript theme={null}
async function waitUntilDone(intentId: string) {
  while (true) {
    const res = await fetch(`${API}/api/2/bridge/status/${intentId}/wait`, {
      headers: { Authorization: "YOUR_API_KEY" },
    });
    const { data } = await res.json();

    if (data.status === "filled" || data.status === "settled") return data;
    if (data.status === "failed" || data.status === "refunded") {
      throw new Error(`Bridge ${data.status}`);
    }
    // pending / deposited / filling / retrying — re-fire immediately
  }
}
```

Don't add a client-side sleep. The server already blocks until something
happens; firing again with no delay keeps one open connection waiting for
the next `NOTIFY`.

#### Stale-response handling

If you start a new bridge while a previous `/wait` is still in flight, the
previous response will arrive **after** your new `intentId` is active. Track
the active intent ID client-side and discard any `/wait` result that doesn't
match it — otherwise you'll attach an old fill TX to the new attempt.

***

## Bridge Routes (`GET /api/2/bridge/routes`)

Returns the static all-to-all matrix of supported routes. Useful for building
a chain selector or validating a pair before calling `/quote`. No query
parameters.

### Supported chains

| Chain ID        | Chain          | Estimated fill latency |
| --------------- | -------------- | ---------------------- |
| `evm:8453`      | Base           | \~500 ms               |
| `evm:56`        | BSC            | \~3000 ms              |
| `evm:42161`     | Arbitrum       | \~1000 ms              |
| `evm:137`       | Polygon        | \~2000 ms              |
| `solana:solana` | Solana         | \~500 ms               |
| `hl:mainnet`    | HyperLiquid L1 | \~1000 ms              |

The route list is all-to-all across these 6 chains excluding same-chain
pairs — 30 entries. Same-chain "bridges" are not in this list; calling
`/quote` with `originChainId === destinationChainId` short-circuits to the
Swap API instead.

### Response

```json theme={null}
{
  "data": {
    "routes": [
      {
        "originChainId": "evm:8453",
        "destinationChainId": "solana:solana",
        "estimatedTimeMs": 1000,
        "maxTradeUsd": 10000,
        "feeBps": 0,
        "supportedTokens": "any"
      }
    ]
  }
}
```

Per-route fields:

* `estimatedTimeMs` — sum of origin and destination chain listener latencies
  from the table above (e.g. Base → Solana = 500 + 500 = 1000).
* `maxTradeUsd` — hard cap per intent. Currently `10000` on every route.
  Amounts above this in `/quote` return `"Amount $X exceeds maximum trade of $10000"`.
* `feeBps` — reported as `0` here; this is the public route metadata. The
  actual solver fee is folded into `/quote`'s `estimatedAmountOut` and
  surfaced as `fees.bridgeFeeBps`. Always read fees from `/quote`, not this
  endpoint.
* `supportedTokens` — `"any"` on every route. The token-level support
  matrix lives in `/quote` (it picks the right code path — native bridge,
  direct-bridge token, swap-and-bridge, or SPL transfer — based on the
  token you pass).
