Bridge
Requires bridge:read for routes, quotes and transfer reads. Building and
submitting a bridge needs both bridge:read and transactions:write -
holding transactions:write for staking or stocks is not enough on its own.
Both can come through the all:read / all:write wildcards.
The bridge moves a user's token from one chain to another - for example USDC from Base to BNB Chain. The user signs once, on the source chain, and needs no native gas token on either side: the batch is sponsored, and the tokens are delivered on the destination without the wallet doing anything.
| Endpoint | Scope | Returns |
|---|---|---|
GET …/bridge/routes | bridge:read | Every direction on offer, with live limits |
POST …/bridge/quote | bridge:read | What an amount would deliver, and what it costs |
GET …/bridge/fee-estimate | bridge:read | The approximate gas fee on an amount |
POST …/bridge/construct/transaction/batch | bridge:read + transactions:write | An unsigned bridge |
POST …/bridge/transaction/batch/send | bridge:read + transactions:write | Submits a signed bridge |
GET …/bridge/transfers | bridge:read | A user's transfers, newest first |
GET …/bridge/transfers/{transferId} | bridge:read | One transfer |
Every transfer status change is also pushed to you as a
bridge_transfer_* callback, on the same endpoint
as your portfolio callbacks.
Routes
Code
Code
Each direction is its own route with its own routeId: Base → BNB Chain and
BNB Chain → Base are two entries, and either can be paused on its own. Treat
this list as the source of truth for what is on offer - do not hard-code a
routeId, and do not assume a direction exists because its reverse does.
The two sides can have different decimals. USDC is 6 decimals on Base and
18 on BNB Chain. Every amount in this API is therefore given twice - a decimal
string in that token's own units ("100.0") and the base-unit integer as a
string ("100000000") - and a raw amount must never be carried from one side
to the other.
Limits are live. They come from the bridge's relayer liquidity at the time
of the call (cached for up to a minute) and move with it. maxAmount is the
largest amount that fills at the quoted speed, in source-token units. When the
bridge cannot be reached, limits is null and quote and construct on that
route answer 503 until it can.
targets lists every contract a batch on this route calls on the source chain:
the bridge's deposit contract, the source token, and the treasury the gas fee is
paid to. See Verifying a batch.
Quoting
Code
Code
amount is in source-token units: 100 is 100 USDC. A quote takes a
userId because the price is for that user's wallet.
There are two costs, and they are paid differently:
providerFeeis what the bridge keeps. It is taken out of the amount: the user depositsamountInand receivesamountOut.gasFeecovers the sponsored gas, and is charged on top of the amount, ingasFeeTokenon the source chain. It is priced from the source chain's current gas price. The wallet must holdamountIn + gasFee, so a "max" button should construct withmaxAmount: truerather than send the balance.
There is no platform fee on a bridge.
A quote is indicative. Construct re-quotes rather than trusting it, so the numbers returned beside the batch are the ones that count.
Estimating the gas fee (fee-estimate)
GET …/fee-estimate?routeId=…&amount=… answers the gas fee on an amount and
finalAmount: amount less that fee. For a wallet holding amount,
finalAmount is about what maxAmount will bridge. It reads no wallet and does
not call the bridge, so it carries no providerFee; quote for that.
The response has the same shape as on
staking, with feePercent and
systemFeeUsd always 0.
Code
The bridge flow
The same three steps as staking and stocks, then tracking:
POST …/bridge/construct/transaction/batchreturns an unsigned EIP-7702 batch user operation on the source chain, anintentId, atransferIdand the quote it was built on.- The user's wallet signs
bundle.userOpHashand, whenbundle.authorizationis notnull, the EIP-7702 authorization. POST …/bridge/transaction/batch/sendreturns the signatures with theintentId, and the API submits the operation.- Track the transfer by
transferId- through callbacks, or by reading it.
1. Build
Code
Code
Send "maxAmount": true instead of amount to bridge the largest amount the
wallet can afford: the source balance less the gas fee, capped at the route's
live maxAmount. A tiny remainder stays in the wallet, so a gas price that
rises before the batch lands cannot make it unaffordable. quote.amountIn in
the response is the amount used. A maximum below the route's minAmount is a
400, as for any amount.
Construct makes two checks a quote does not, both before anything is signed:
- the wallet holds
amount + gasFeeof the source token - otherwise a400naming both figures; - the bridge's fee is within the ceiling we set for the route - otherwise a
503; try again shortly.
2. Sign
Exactly as for staking and stocks:
signature- EIP-191 over the 32 bytes ofbundle.userOpHash, prefixed with0xff00.authorization- the EIP-7702 tuple signed as{ r, s, yParity }, usingbundle.authorization. Note itschainIdis the source chain. Only whenbundle.authorizationis notnull; for a wallet already delegated on the source chain it isnull, and the submit body leavesauthorizationout.
Signing with Privy and Signing with Wallet Core cover both signatures in code.
3. Submit
Code
Code
Submit before quote.expiresAt - a bridge intent lives only as long as its
quote is fillable, a few minutes at most. A late or repeated submit is a 400;
build and sign a new one. Only an intent built by the bridge construct is
accepted here, and a bridge must be submitted here, not through the staking or
stocks submit endpoint.
4. Track
Code
Code
This body is exactly what every bridge callback carries.
GET …/bridge/transfers?userId=… lists a user's transfers, newest first.
Filter with status=, and page with limit (up to 100) and the nextCursor
of the previous page passed back as cursor. nextCursor is null on the
last page.
Statuses
| Status | Meaning | Callback |
|---|---|---|
pending | Built; the wallet has not signed | - |
submitted | The signed operation reached the bundler | bridge_transfer_submitted |
source_confirmed | The deposit landed on the source chain; the gas fee is paid | bridge_transfer_source_confirmed |
delivered | The tokens arrived on the destination - usually seconds later | bridge_transfer_delivered |
refund_pending | Nobody filled the deposit before its deadline; it is being refunded | bridge_transfer_refund_pending |
refunded | The refund landed back on the source chain | bridge_transfer_refunded |
failed | The operation reverted or never reached the bundler. Nothing moved | bridge_transfer_failed |
expired | Built but never signed in time | - |
delivered, refunded, failed and expired are final.
A refund is slow and lands on the source chain. It is paid back to the same wallet on the chain the tokens left from, in a later settlement by the bridge
- hours, not seconds. The money is never lost, but tell the user where and when to expect it.
Verifying a batch
Every call in a bridge batch is plain, decodable calldata, so you can check a batch before the user signs it without trusting our API. On the source chain, in this order, and nothing else:
approve(bridge, amount)on the source token - for the exact amount, never unlimited;- the bridge deposit on
targets.bridge, with the user's own wallet as both depositor and recipient, the input and output token and amount, and the destination chain id all in the calldata, and no attached message; transfer(treasury, gasFee)on the source token - omitted when no gas fee is charged.
Every target is in the route's targets, so a verifier can allowlist them. A
contract-based verifier and its documentation are on the way and will be linked
here.
Failure modes
| Status | Cause |
|---|---|
400 | The route is paused; the amount is below minAmount or above maxAmount (the message names the limit); the user has no linked wallet; the wallet does not hold amount + gasFee (INSUFFICIENT_BALANCE); or the intent was already submitted or has expired |
403 | Your token lacks bridge:read, or transactions:write on construct or submit |
404 | No such route or transfer - or no such user, or a user or transfer belonging to another partner |
503 | The bridge could not be reached, its fee is above the route's ceiling right now, or the quote expires too soon to sign. Retry shortly |
