Skip to main content
Bridge, swap, and interact across chains in one transaction. User signs once—MEE handles timing, confirmations, and execution.

How It Works

  1. User signs a single message
  2. MEE executes operations on the source chain
  3. MEE waits for bridge completion
  4. MEE executes operations on the destination chain
  5. User receives final tokens

Basic Cross-Chain Flow

import { toMultichainNexusAccount, createMeeClient } from "@biconomy/abstractjs";
import { arbitrum, base } from "viem/chains";

// Account spans multiple chains
const account = await toMultichainNexusAccount({
  signer,
  chainConfigurations: [
    { chain: arbitrum, transport: http(), version: getMEEVersion(MEEVersion.V2_1_0) },
    { chain: base, transport: http(), version: getMEEVersion(MEEVersion.V2_1_0) }
  ]
});

const meeClient = await createMeeClient({ account });

// Instructions on different chains
const bridgeInstruction = /* bridge from Arbitrum */;
const swapInstruction = /* swap on Base */;
const depositInstruction = /* deposit on Base */;

const quote = await meeClient.getQuote({
  instructions: [bridgeInstruction, swapInstruction, depositInstruction],
  feeToken: { address: USDC_ARBITRUM, chainId: arbitrum.id }
});

const { hash } = await meeClient.executeQuote({ quote });

Runtime Injection for Bridge Outputs

Since bridge outputs aren’t known upfront, use runtimeERC20BalanceOf:
import { runtimeERC20BalanceOf, greaterThanOrEqualTo } from "@biconomy/abstractjs";

// Bridge USDC from Arbitrum to Base via Across SpokePool
// See the Across integration guide for full setup: /new/integration-guides/bridges-and-solvers/integrate-across
const bridge = await account.buildComposable({
  type: "default",
  data: {
    chainId: arbitrum.id,
    to: ACROSS_SPOKE_POOL,
    abi: acrossSpokePoolAbi,
    functionName: "depositV3",
    args: [
      account.addressOn(arbitrum.id, true),  // depositor
      account.addressOn(base.id, true),       // recipient
      USDC_ARBITRUM,                          // inputToken
      USDC_BASE,                              // outputToken
      parseUnits("100", 6),                   // inputAmount
      outputAmount,                           // outputAmount (from Across quote API)
      base.id,                                // destinationChainId
      zeroAddress,                            // exclusiveRelayer
      quoteTimestamp,                         // quoteTimestamp (from Across quote API)
      fillDeadline,                           // fillDeadline
      0,                                      // exclusivityDeadline
      "0x"                                    // message
    ]
  }
});

// Swap on Base—waits for bridge to complete
const swap = await account.buildComposable({
  type: "default",
  data: {
    chainId: base.id,
    to: UNISWAP_ROUTER,
    abi: UniswapAbi,
    functionName: "exactInputSingle",
    args: [{
      tokenIn: USDC_BASE,
      amountIn: runtimeERC20BalanceOf({
        tokenAddress: USDC_BASE,
        targetAddress: account.addressOn(base.id, true),
        constraints: [greaterThanOrEqualTo(parseUnits("80", 6))]  // 20% slippage tolerance
      }),
      tokenOut: WETH_BASE,
      // ...
    }]
  }
});
Automatic Sequencing: MEE retries the swap instruction until the constraint is satisfied (bridge tokens have arrived).

Fusion Mode for External Wallets

For MetaMask/Rabby users:
const trigger = {
  chainId: arbitrum.id,
  tokenAddress: USDC_ARBITRUM,
  amount: parseUnits("100", 6)
};

const quote = await meeClient.getFusionQuote({
  trigger,
  instructions: [
    approveAcross,
    bridge,
    swapOnBase,
    depositOnBase,
    returnTokensToEOA
  ],
  feeToken: { address: USDC_ARBITRUM, chainId: arbitrum.id }
});

const { hash } = await meeClient.executeFusionQuote({ fusionQuote: quote });

Example: Bridge → Swap → Deposit

Complete flow: USDC on Arbitrum → WETH yield on Base
const inputAmount = parseUnits("100", 6);
const minAfterSlippage = inputAmount * 80n / 100n;

// 1. Approve Across to spend USDC
const approveAcross = await account.buildComposable({
  type: "approve",
  data: { spender: ACROSS_SPOKE_POOL, tokenAddress: USDC_ARBITRUM, chainId: arbitrum.id, amount: inputAmount }
});

// 2. Bridge USDC: Arbitrum → Base (via Across SpokePool)
const bridge = await account.buildComposable({
  type: "default",
  data: {
    chainId: arbitrum.id,
    to: ACROSS_SPOKE_POOL,
    abi: acrossSpokePoolAbi,
    functionName: "depositV3",
    args: [
      account.addressOn(arbitrum.id, true),  // depositor
      account.addressOn(base.id, true),       // recipient
      USDC_ARBITRUM,                          // inputToken
      USDC_BASE,                              // outputToken
      inputAmount,                            // inputAmount
      outputAmount,                           // outputAmount (from Across quote API)
      base.id,                                // destinationChainId
      zeroAddress,                            // exclusiveRelayer
      quoteTimestamp,                         // quoteTimestamp
      fillDeadline,                           // fillDeadline
      0,                                      // exclusivityDeadline
      "0x"                                    // message
    ]
  }
});

// 3. Swap USDC → WETH on Base (waits for bridge)
const swap = await account.buildComposable({
  type: "default",
  data: {
    chainId: base.id,
    to: UNISWAP_ROUTER,
    abi: UniswapAbi,
    functionName: "exactInputSingle",
    args: [{
      tokenIn: USDC_BASE,
      amountIn: runtimeERC20BalanceOf({
        tokenAddress: USDC_BASE,
        targetAddress: account.addressOn(base.id, true),
        constraints: [greaterThanOrEqualTo(minAfterSlippage)]
      }),
      tokenOut: WETH_BASE,
      amountOutMinimum: 0n,
      recipient: account.addressOn(base.id, true),
      fee: 500,
      sqrtPriceLimitX96: 0n
    }]
  }
});

// 4. Deposit WETH into Morpho
const deposit = await account.buildComposable({
  type: "default",
  data: {
    chainId: base.id,
    to: MORPHO_WETH_POOL,
    abi: MorphoAbi,
    functionName: "deposit",
    args: [
      runtimeERC20BalanceOf({ tokenAddress: WETH_BASE, targetAddress: account.addressOn(base.id, true) }),
      account.addressOn(base.id, true)
    ]
  }
});

// Execute entire flow
const quote = await meeClient.getQuote({
  instructions: [approveAcross, bridge, swap, deposit],
  feeToken: { address: USDC_ARBITRUM, chainId: arbitrum.id }
});

const { hash } = await meeClient.executeQuote({ quote });

Key Benefits

TraditionalWith MEE
4+ signatures1 signature
Manual bridge waitingAutomatic
ETH needed on both chainsPay in one token
Risk of stuck statesNo stuck states—programmatic fund retrieval via cleanups
Important: Cross-chain transactions are not atomic—only single-chain batches are fully atomic (all-or-nothing). For cross-chain flows, if a step fails after bridging, tokens won’t be lost—use cleanup transactions to programmatically return funds to the user’s wallet.

Next Steps

Composable Batching

Deep dive into dynamic parameters