Skip to main content
POST
Execute a gasless (sponsored) EVM deposit
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.
POST /api/2/bridge/execute broadcasts an EVM origin deposit on the user’s behalf and pays its gas. The user signs the batch off-chain; Mobula’s solver sends it. A wallet with 0 ETH/BNB/POL can approve and bridge in a single signature. The origin gas is not free — it’s priced into the quote and deducted from the amount delivered, which is why /execute only accepts intents quoted with gasless=true.

How it works

Under the hood this is an EIP-7702 sponsored batch:
  1. The user’s EOA is delegated to MobulaBatchExecutor. The delegation rides along as an authorization tuple on the first sponsored transaction — the wallet never sends a delegation transaction of its own.
  2. The user signs an EIP-712 Batch — the quote’s approve + bridgeToken/swapAndBridge steps, exactly the transactions they would otherwise broadcast themselves.
  3. Mobula submits a type-4 transaction carrying that batch and pays the gas. The calls execute from the user’s own EOA, so msg.sender at the bridge is still the user, and the deposit is indistinguishable from a self-sent one (same refund path, same /status lifecycle).
Because the calls run from the user’s account, Mobula never takes custody and never needs an allowance to itself — the approve in the batch is the user’s own approve.

Authentication

Same as every bridge endpoint, and required — the call is refused with 401 without it. Pass your API key as a query parameter on the POST URL (POST /api/2/bridge/execute?apiKey=YOUR_API_KEY) or send it as Authorization: Bearer <apiKey-or-short-lived-JWT>, exactly like /quote. The request costs one credit on the resolved key.

Availability

Don’t hardcode the executor address. Every gasless quote returns it as sponsorGate.batchExecutor, and the authorization tuple must name whatever that says. A client holding its own copy is a second source of truth for the address its batch is signed against: when the executor is redeployed, the batch is built for one delegate and submitted against an account still delegated to another, which does not necessarily fail loudly. Since no intent can reach /execute without a gasless=true quote first, reading it from the quote costs nothing. It is currently the same address on every supported chain, but that is a fact about today’s deployment, not a guarantee:

The flow

Two of those signatures are the normal bridge flow (the intent) and the batch; the authorization only appears once per account per chain. You can send the bridge-intent signature in the /execute body to skip the signed-quote confirm call/execute persists the intent from the quote’s own stash, so the whole trade is two HTTP calls.

Request body

Batch EIP-712 schema

gasToken and minBalance are a balance gate the executor checks before any call runs: if the account does not hold minBalance of gasToken, the batch reverts immediately. It exists because Mobula pays the gas whether a batch succeeds or fails, so a deposit that was never going to move any tokens has to fail in ~35k gas rather than burning the whole limit. Set them to the origin token and the exact amount the quote priced — /execute refuses a batch whose gasToken is not the quoted origin token, or whose minBalance is below the quoted deposit. verifyingContract is the user’s own account — that’s what stops a batch signed for one delegated EOA from being replayed against another.

Authorization tuple

  • address must be the chain’s MobulaBatchExecutor; anything else is refused.
  • chainId must be the origin chain — the 0 “any chain” wildcard is refused.
  • nonce is the authority’s current account nonce, not nonce + 1: the solver submits the transaction, not the user. With viem’s signAuthorization, that means not passing executor: 'self'. Get this wrong and the chain silently drops the authorization, leaving the batch to call execute on an account with no code.
Read the current delegation with eth_getCode(authority): an empty result is a plain EOA, and a delegated one is exactly 0xef0100 || <20-byte delegate>. Send a tuple when that delegate isn’t MobulaBatchExecutor.

Nonce lanes

MobulaBatchExecutor uses a two-dimensional nonce — key << 64 | seq, sequential within a lane, independent across lanes — and every intent gets its own lane derived from its intentId. Two deposits signed before either mines can’t collide, and a fresh lane always starts at sequence 0, so no client ever reads the chain to pick a nonce. Re-signing the same intent reuses its lane, which is what makes a retry mutually exclusive with the attempt it replaces.

Response

  • depositTxHash is the sponsored transaction carrying the batch.
  • sponsoredGasLimit is the gas units the send was capped at — exactly the units the quote charged for.
  • status is always pending: the deposit has been broadcast, not yet filled. Poll /status/{intentId}/wait as with any other deposit.

What the server checks

A sponsored batch spends Mobula’s gas, so /execute validates rather than rebuilds it (rebuilding would change the calldata and void the user’s signature):
  • The intent’s prediction must say it was quoted gasless, and quoted for this authority.
  • Every calls[].to must be one of three addresses: the origin token the quote priced (to approve), the chain’s SwapBridgeHelper, or MobulaBridge. Anything else is out of scope.
  • No call may carry native value.
  • batchNonce must be this intent’s lane; deadline must be in the future; at most 4 calls.
  • batchSignature must recover to authority, and every authorization tuple must be signed by authority and delegate to MobulaBatchExecutor on this chain.
  • The gas limit of the broadcast is the quote’s own units — never a client- or solver-supplied number.

Idempotency and retries

intentId is the idempotency key: the first /execute for an intent claims it for 15 minutes, and a second returns 409. On a 504 the claim is deliberately retained — the broadcast may still land, so poll /status/{intentId} before doing anything else. Every other failure releases the claim, so you can fix the request and retry the same intent.

When the batch reverts on-chain

A 200 means the transaction was broadcast, not that it succeeded. A batch can still revert once mined — a stale approval, a swap that moves past its own limit, an out-of-gas. When it does, no tokens moved, so no deposit exists and no bridge intent is ever created. /status/{intentId} reports that case as failed rather than leaving it pending:
Nothing left the user’s wallet, so there is nothing to refund. Re-quote and sign again — re-submitting the same batch is refused with a 409, because it would revert identically.

Errors

Error bodies are { "error": "...", "intentId": "..." }.

Example

Base → BSC, 100 USDC → USDT, from a wallet with no ETH. Two HTTP calls, three signatures (one of which disappears after the first ever gasless deposit).

Cost model

The user pays no origin gas, but the trade does. On a gasless=true quote:
  • fees.originSponsorGasUsd is what the sponsored send costs, and it’s already deducted from estimatedAmountOut and included in totalFeeUsd.
  • The signed minAmountOut is derived from that same netted amount, so the floor the solver enforces matches the number the user was shown.
  • The units charged are the exact gas limit the broadcast gets — the batch can never burn more than the quote reserved.
If the amount is too small to cover the origin gas, the quote fails with "Amount does not cover the sponsored origin gas ($…)" rather than quoting a payout it can’t honour.

See also

Body

application/json
originChainId
string
required

Origin chain ID of the quote (e.g., "evm:8453"). Must be an EVM chain with a MobulaBatchExecutor.

intentId
string
required

The intentId returned by the gasless=true quote.

authority
string
required

The user's EOA — signer of the batch, and the account the batched calls execute from. Must match the address the intent was quoted for.

calls
object[]
required

The quote's steps, in order, as batch calls (approve → bridgeToken/swapAndBridge). Max 4, each with value "0".

batchNonce
string
required

MobulaBatchExecutor nonce, decimal string. Must equal this intent's lane: (keccak256(utf8Bytes(intentId)) & (2**192 - 1)) << 64.

deadline
string
required

Unix seconds after which the batch signature is refused. Must be in the future.

gasToken
string
required

Token whose balance gates the batch on-chain, taken verbatim from the quote's sponsorGate.gasToken. Must be the token this intent was quoted for; anything else is refused.

minBalance
string
required

Minimum gasToken balance the account must hold for the batch to run, from the quote's sponsorGate.minBalance. Must be at least the quoted deposit amount. The executor checks it before any call runs, so a batch that was never going to move tokens fails in ~35k gas instead of burning its whole limit.

batchSignature
string
required

65-byte EIP-712 signature by authority over the Batch(calls, nonce, deadline, gasToken, minBalance) payload (domain MobulaBatchExecutor v1, verifyingContract = authority).

shape
enum<string>
required

Which bridge entrypoint the batch ends on, matching the quote's last step type.

Available options:
bridgeToken,
swapAndBridge
executionKind
enum<string>

Execution mechanism. Only sponsored7702 exists today; the field is optional and reserved.

Available options:
sponsored7702
authorizationList
object[]

EIP-7702 authorization tuples. Exactly one on the account's first sponsored send (it delegates the EOA to MobulaBatchExecutor in the same transaction); empty afterwards.

signature
string

Optional EIP-712 bridge-intent signature over the quote's typedData. Send it here to skip the signed-quote confirm call — /execute persists the intent from the quote's stash. Omit if you already committed the signature via GET /2/bridge/quote.

Response

The sponsored deposit was broadcast.

data
object
required