Skip to main content
GET
/
2
/
bridge
/
quote
Get bridge quote
curl --request GET \
  --url https://demo-api.mobula.io/api/2/bridge/quote
{
  "data": {
    "estimatedAmountOut": "<string>",
    "estimatedAmountOutUsd": "<string>",
    "fees": {
      "bridgeFeeBps": 123,
      "gasFeeUsd": "<string>",
      "totalFeeUsd": "<string>"
    },
    "estimatedTimeMs": 123,
    "maxTradeUsd": 123,
    "deposit": {
      "evm": {
        "to": "<string>",
        "data": "<string>",
        "value": "<string>",
        "chainId": 123,
        "approvalAddress": "<string>",
        "approvalToken": "<string>",
        "approvalAmount": "<string>"
      },
      "solana": {
        "to": "<string>",
        "amount": "<string>",
        "memo": "<string>",
        "serializedTx": "<string>"
      },
      "hl": {
        "to": "<string>",
        "token": "<string>",
        "amount": "<string>"
      }
    },
    "steps": [
      {
        "type": "<string>",
        "tx": {
          "to": "<string>",
          "data": "<string>",
          "value": "<string>",
          "chainId": 123,
          "approvalAddress": "<string>",
          "approvalToken": "<string>",
          "approvalAmount": "<string>"
        },
        "description": "<string>"
      }
    ]
  },
  "error": "<string>"
}
Alpha Preview — Endpoints, response shape, contract addresses, and supported routes may change without notice. Don’t depend on it for production-critical flows until it leaves alpha.
GET /api/2/bridge/quote returns a ready-to-sign deposit transaction plus a 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

NameRequiredNotes
apiKeyyesYour Mobula API key, passed in the query string (?apiKey=…). /quote reads it from the query, not an Authorization header — omitting it returns { "error": "Missing required parameter: apiKey" }.
originChainIdyesOne of the supported chain IDs (see Bridge Routes).
destinationChainIdyesSame set. When equal to originChainId, the endpoint short-circuits to the Swap API — see Same-chain quotes.
walletAddressyesDestination recipient. Format-validated against the destination chain: EVM regex for evm:* and hl:mainnet, Base58 for solana:solana.
amountyes (cross-chain)Decimal human units ("0.05", not wei). Must be finite, positive, ≤ 1e15. Not required when source and destination are the same chain.
originTokennoOmit, or pass 0x0000…0000 / 0xeeee…eeee for the native token. EVM addresses are checksummed server-side.
destinationTokennoSame rules. When omitted on a Solana destination, the API substitutes wSOL (So11111111111111111111111111111111111111112).
senderAddressconditionalRequired for Solana SPL bridges (the controller needs it to build the ATA + SPL transfer + memo). For HL origin, this is the HL spot wallet that will sign — it must equal the EIP-712 signer (see Signed-quote flow). Optional for EVM origins (defaults to walletAddress).
slippagenoPercent. Default 1, valid range 0 to 50.
signatureconditionalEIP-712 signature over the typed-data payload returned by the unsigned call. Required for evm:* and hl:mainnet origins to commit the prediction. Omit on the first call to preview the quote + receive the typedData to sign. See Signed-quote flow.
intentIdconditionalEcho back the intentId returned by the unsigned call when submitting signature. Format xxxxxxx-xxxxxxx-xxx (lowercase hex).
deadlineconditionalEcho back the deadline returned by the unsigned call. Unix seconds. Server rejects expired deadlines.
minAmountOutconditionalEcho back the minAmountOut (raw destination-token units) from the typedData. The signed value is authoritative — the server reconstructs the typedData from this exact value before verifying the signature.
Validation errors come back as { "error": "..." } with HTTP 200 (or HTTP 400 for invalid signatures) — always check the error key before reading data.

Response

{
  "data": {
    "intentId": "a3b4ba1-e34523c-324",
    "deadline": 1764032100,
    "typedData": { /* EIP-712 payload to sign — see Signed-quote flow */ },
    "signatureRequired": true,
    "prediction": { "persisted": false },
    "estimatedAmountOut": "0.68421052",
    "estimatedAmountOutUsd": "1.99",
    "recommendedSlippage": 1.5,
    "fees": {
      "bridgeFeeBps": 0,
      "bridgeFeeUsd": "0.0000",
      "destFillGasUsd": "0.0800",
      "originRefundGasUsd": "0.0200",
      "gasFeeUsd": "0.1000",
      "totalFeeUsd": "0.1000"
    },
    "estimatedTimeMs": 1000,
    "maxTradeUsd": 10000,
    "steps": [ /* optional, see below */ ],
    "deposit": { /* one of evm | solana | hl */ }
  }
}
  • intentId is the user-facing handle, format xxxxxxx-xxxxxxx-xxx (lowercase hex). 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. You must echo this exact intentId back via the intentId query param when submitting the signature — the signed payload binds to it.
  • deadline is Unix seconds. The server rejects signed calls past this point.
  • typedData is the EIP-712 structured-data payload your wallet should sign (see Signed-quote flow for the full schema).
  • signatureRequired: true for evm:* and hl:mainnet origins. false for solana:solana (the depositor-signed memo replaces the signature).
  • prediction.persisted indicates whether the server wrote the prediction row. false on the unsigned preview call; true after a successful signed call.
  • 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/HL bridges omit steps.
  • recommendedSlippage is the slippage % we suggest signing with — the route’s implied spread ((1 − outUsd/inUsd)×100) plus a 0.5% drift buffer, rounded up to 0.1, floored at 1, and capped at 50 (null when USD prices are unavailable). The solver refunds any fill below the signed minAmountOut (failure code slippage), so a tolerance under this value is a near-guaranteed refund. If your slippage is below it, re-quote at the recommendation before signing.
  • fees are real, deducted amounts — there is no placeholder. bridgeFeeUsd is the Mobula protocol fee (bridgeFeeBps, currently 0); destFillGasUsd is what the solver pays to fill on the destination chain; originRefundGasUsd is a refund-gas reserve charged on every quote so a refund never puts the solver in the red. gasFeeUsd = destFillGasUsd + originRefundGasUsd and totalFeeUsd is the sum. estimatedAmountOut / estimatedAmountOutUsd are already net of all of it; the user additionally pays only 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)

{
  "to": "0xa834E70303322D86E5DaE95ee47E9c6a073d9812",
  "data": "0x...",
  "value": "50000000000000000",
  "chainId": 8453,
  "approvalAddress": "0x...",      // present for ERC-20 origins
  "approvalToken": "0x...",        // present for ERC-20 origins
  "approvalAmount": "115792...255" // MAX_UINT256
}
Three code paths:
  • Native ETH/BNB/POL — single bridge() call on MobulaBridge. value is the raw amount in wei. No steps.
  • Direct-bridge tokensapprove step to MobulaBridge, then bridgeToken(). value is "0". The set is USDC on all four chains, plus Base’s bridged USDbC and USDT on Arbitrum and Polygon (BSC is USDC-only).
  • Any other ERC-20approve 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 (MobulaBridgeV2, deployed 2026-06-04) is the same proxy address on every EVM chain:
ChainBridge contract
evm:8453 (Base)0xa834E70303322D86E5DaE95ee47E9c6a073d9812
evm:56 (BSC)0xa834E70303322D86E5DaE95ee47E9c6a073d9812
evm:42161 (Arbitrum)0xa834E70303322D86E5DaE95ee47E9c6a073d9812
evm:137 (Polygon)0xa834E70303322D86E5DaE95ee47E9c6a073d9812
The swap-and-bridge path approves a separate per-chain SwapBridgeHelper (the spender named in the approve step) — always approve the spender the step specifies, not a hardcoded address. 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)

{
  "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.

Signed-quote flow

For evm:* and hl:mainnet origins the destination token (and, for HL, the destination address) is not committed on chain — it lives in a server-side prediction row keyed on the depositor’s address. Without binding that row to the depositor’s key, anyone who knows the address can overwrite the row and redirect funds to a different token. To close that hole, /quote enforces an EIP-712 signature flow. The flow is two API calls plus one wallet signature:
  1. Preview callGET /quote?... with no signature. Server returns the quote, the deposit calldata, and a typedData payload. No prediction row is written.
  2. Sign — your wallet signs typedData (e.g. eth_signTypedData_v4 for injected wallets, walletClient.signTypedData with viem). The signer’s address must equal typedData.message.sender.
  3. Commit callGET /quote?...&signature=<sig>&intentId=<id>&deadline=<ts>&minAmountOut=<raw> with the same input params as call 1. Server reconstructs the typedData from the query params, recovers the signer, and writes the prediction row (with the signature persisted) if everything lines up. Returns the same response shape with prediction.persisted: true.
  4. Broadcast — submit deposit.evm (or deposit.hl) on chain.

EIP-712 schema

Domain:
  name:    "Mobula Bridge"
  version: "1"
  chainId: <numeric origin chain id>   // 8453, 56, 42161, 137 — and 1 (NOT 999) for hl:mainnet

BridgeIntent:
  intentId           string    — same as `data.intentId` (dashed hex, signed as a string)
  sender             address   — depositor EVM key (HL origin: HL spot wallet; EVM: walletAddress)
  originChainId      string    — e.g. "evm:8453"
  originToken        string    — "0x000…0" for native, or token address / HL token id
  amountIn           uint256   — raw origin-token units
  destinationChainId string
  destinationToken   string
  recipient          bytes32   — EVM dest: address left-padded; Solana dest: bs58-decoded
  minAmountOut       uint256   — raw destination-token units, echoed back as the `minAmountOut` query param
  deadline           uint64    — Unix seconds, echoed back as the `deadline` query param
viem omits the EIP712Domain type from types — inject it client-side (name:string, version:string, chainId:uint256) before signing if your signer needs it. HyperLiquid specifics. HL signs all actions under Ethereum mainnet, so for hl:mainnet origins typedData.domain.chainId is 1 (not 999) — switch the wallet to chain 1 before signing. An HL bridge then needs two signatures: (1) this EIP-712 BridgeIntent confirm, then (2) the HL spotSend transfer to the solver.

Failure modes on the commit call

  • 400 Invalid bridge intent signature: ... — signature did not recover to the depositor address. Re-check typedData.message.sender and the signer.
  • 400 Signature deadline has expired or is invaliddeadline is past now. Re-quote.
  • 400 Signed-mode quote requires intentId, deadline, minAmountOut, and signature — one of the four signed-mode params is missing.

Side effects

/quote upserts a row into misc.bridge_intent_predictions keyed by (sender, originChainId, destinationChainId) only when called with a valid signature (Solana origin: also writes on every call, since the memo carries the binding). The destination-chain listener reads the row back to resolve destinationToken, recipient, and slippage when the deposit lands. The solver rejects deposits whose prediction row has signature IS NULL for EVM/HL origins.

Example

EVM origin (signed-quote flow, Base → BSC, 100 USDC → USDT):
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const account = privateKeyToAccount("0xYourPrivKey");
const client = createWalletClient({ account, chain: base, transport: http() });

// 1. Preview call (no signature, no prediction row written).
const baseParams = new URLSearchParams({
  originChainId: "evm:8453",
  destinationChainId: "evm:56",
  originToken: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // USDC on Base
  destinationToken: "0x55d398326f99059fF775485246999027B3197955", // USDT on BSC
  amount: "100",
  walletAddress: account.address,
  senderAddress: account.address,
  apiKey: "YOUR_API_KEY",
});

const preview = await fetch(`https://api.mobula.io/api/2/bridge/quote?${baseParams}`)
  .then((r) => r.json());
if (preview.error) throw new Error(preview.error);
const { intentId, deadline, typedData, estimatedAmountOut } = preview.data;

// 2. Sign the typed data.
const signature = await client.signTypedData({
  account,
  domain: typedData.domain,
  types: typedData.types,
  primaryType: typedData.primaryType,
  message: typedData.message,
});

// 3. Commit call (server verifies + writes prediction row).
const commitParams = new URLSearchParams(baseParams);
commitParams.set("intentId", intentId);
commitParams.set("deadline", String(deadline));
commitParams.set("minAmountOut", typedData.message.minAmountOut);
commitParams.set("signature", signature);

const committed = await fetch(`https://api.mobula.io/api/2/bridge/quote?${commitParams}`)
  .then((r) => r.json());
if (committed.error) throw new Error(committed.error);

// 4. Broadcast the on-chain deposit.
const tx = committed.data.deposit.evm; // approve+bridgeToken steps in committed.data.steps
const hash = await client.sendTransaction({
  to: tx.to,
  data: tx.data,
  value: BigInt(tx.value),
});
console.log("Deposit tx:", hash);
console.log("Poll status with:", intentId);
For a full sign-and-broadcast walkthrough across EVM, Solana, and HyperLiquid origins, see the Bridge Implementation guide. After broadcasting the deposit, poll /status/:id/wait with quote.intentId.

Query Parameters

originChainId
string
required

Origin chain ID (e.g., "evm:8453", "solana:solana", "hl:mainnet")

destinationChainId
string
required

Destination chain ID (e.g., "evm:8453", "solana:solana", "hl:mainnet")

amount
string
required

Human-readable amount of origin token to bridge (e.g., "0.1")

walletAddress
string
required

Recipient wallet address on the destination chain (EVM hex, Solana base58, or HL hex).

originToken
string

Origin token contract/mint address. Omit or pass the zero address for the native token.

destinationToken
string

Destination token contract/mint address. Omit or pass the zero address for the native token.

slippage
string

Slippage tolerance in percent (0-50, default: 1).

senderAddress
string

Origin-chain sender address. Required when bridging an SPL token from Solana (the wallet signing the swap+deposit TX).

Response

200 - application/json

Bridge quote response. Either data is populated with quote + deposit instructions, or error describes why the quote could not be built.

data
object
error
string

Error message when the quote could not be built (missing params, unsupported route, price unavailable, amount over cap, swap failed, …).