Skip to main content
Cross-chain Composer lets users deposit into protocols on a different chain from where their assets live, all in a single flow. This guide covers the patterns, examples, and considerations for cross-chain Composer operations.

How Cross-Chain Composer Works

When fromChain and toChain differ, LI.FI’s routing engine automatically combines:
  1. Source chain actions — swap tokens if needed
  2. Bridge — transfer assets to the destination chain
  3. Destination chain actions — swap to the required token and deposit into the target protocol via Composer
From the developer’s perspective, the API call is identical to same-chain Composer. You just use different chain IDs. LI.FI handles bridge selection, intermediate swaps, and the final Composer deposit.
The patterns below illustrate possible execution paths. You do not control which pattern the routing engine selects. It optimises for cost, speed, and liquidity automatically. You will only know the exact execution path when you receive the route response.
Source Chain (e.g., Ethereum)          Destination Chain (e.g., Base)
┌──────────────────────────┐          ┌──────────────────────────┐
│  1. Swap ETH → USDC      │          │  4. Receive bridged USDC  │
│     (if needed)           │          │  5. Deposit USDC into     │
│  2. Approve bridge        │── bridge ──▶│     Morpho vault       │
│  3. Send to bridge        │          │  6. Return vault tokens   │
└──────────────────────────┘          └──────────────────────────┘

Common Cross-Chain Patterns

These patterns describe typical ways the routing engine may fulfil a cross-chain Composer request. The actual path is determined at quote time based on available liquidity, bridge options, and gas costs.

Bridge + Deposit

User has the right token on the wrong chain. Example: USDC on Arbitrum → Morpho vault on Base
curl -X GET 'https://li.quest/v1/quote?\
fromChain=42161&\
toChain=8453&\
fromToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&\
toToken=0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A&\
fromAddress=0xYOUR_WALLET_ADDRESS&\
toAddress=0xYOUR_WALLET_ADDRESS&\
fromAmount=10000000'
What may happen under the hood:
  1. Bridge USDC from Arbitrum to Base
  2. Deposit USDC into Morpho vault on Base
  3. User receives vault tokens
The exact steps depend on the route returned by the API. The routing engine may choose a different bridge or insert intermediate swaps if that produces a better outcome.

Swap + Bridge + Deposit

User has a different token on a different chain. Example: ETH on Ethereum → Aave USDC lending on Optimism
curl -X GET 'https://li.quest/v1/quote?\
fromChain=1&\
toChain=10&\
fromToken=0x0000000000000000000000000000000000000000&\
toToken=AAVE_AUSDC_TOKEN_ADDRESS_ON_OPTIMISM&\
fromAddress=0xYOUR_WALLET_ADDRESS&\
toAddress=0xYOUR_WALLET_ADDRESS&\
fromAmount=100000000000000000'
What may happen under the hood:
  1. Swap ETH → USDC on Ethereum, then bridge USDC to Optimism, or bridge ETH directly and swap on the destination chain
  2. Deposit USDC into Aave on Optimism
  3. User receives aUSDC tokens
The routing engine decides the optimal sequence. You will see the chosen path in the route response.

Bridge + Swap + Stake

User wants to stake on a different chain. Example: USDC on Arbitrum → wstETH (Lido) on Ethereum
const quote = await getQuote({
  fromChain: 42161,                                             // Arbitrum
  toChain: 1,                                                   // Ethereum
  fromToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',     // USDC on Arbitrum
  toToken: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0',       // wstETH on Ethereum
  fromAmount: '1000000000',                                     // 1000 USDC
  fromAddress: '0xYOUR_WALLET_ADDRESS',
});
What may happen under the hood:
  1. Bridge USDC from Arbitrum to Ethereum
  2. Swap USDC → ETH on Ethereum
  3. Stake ETH via Lido, wrap to wstETH
  4. User receives wstETH
The routing engine may choose a different sequence (e.g., swap on Arbitrum first, then bridge ETH) if it finds a better path.

Execution Flow

With the API

Cross-chain Composer via the API requires status polling since the bridge step is asynchronous:
import axios from 'axios';
import { ethers } from 'ethers';

const API_URL = 'https://li.quest/v1';

// 1. Get quote
const quote = await getQuote({
  fromChain: 42161,
  toChain: 8453,
  fromToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  toToken: '0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A',
  fromAmount: '10000000',
  fromAddress: await signer.getAddress(),
});

// 2. Approve (if needed)
if (quote.estimate.approvalAddress) {
  await ensureAllowance(
    signer,
    quote.action.fromToken.address,
    quote.estimate.approvalAddress,
    quote.action.fromAmount
  );
}

// 3. Execute
const tx = await signer.sendTransaction(quote.transactionRequest);
await tx.wait();
console.log('Source chain tx confirmed:', tx.hash);

// 4. Poll status until complete
let status;
do {
  status = await axios.get(`${API_URL}/status`, {
    params: {
      txHash: tx.hash,
      fromChain: quote.action.fromChainId,
      toChain: quote.action.toChainId,
    },
  }).then(r => r.data);

  console.log(`Status: ${status.status} - ${status.substatus || ''}`);

  if (status.status !== 'DONE' && status.status !== 'FAILED') {
    await new Promise(r => setTimeout(r, 5000)); // Poll every 5 seconds
  }
} while (status.status !== 'DONE' && status.status !== 'FAILED');

if (status.status === 'DONE') {
  console.log('Cross-chain Composer deposit complete!');
  console.log('Receiving tx:', status.receiving?.txHash);
} else {
  console.error('Transfer failed:', status.substatus);
}

With the SDK

The SDK handles the entire cross-chain flow automatically:
import { createConfig, getQuote, convertQuoteToRoute, executeRoute } from '@lifi/sdk';

createConfig({ integrator: 'YourAppName' });

const quote = await getQuote({
  fromChain: 42161,
  toChain: 8453,
  fromToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  toToken: '0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A',
  fromAmount: '10000000',
  fromAddress: '0xYOUR_WALLET_ADDRESS',
});

const route = convertQuoteToRoute(quote);
const result = await executeRoute(route, {
  updateRouteHook(updatedRoute) {
    for (const step of updatedRoute.steps) {
      const lastProcess = step.execution?.process?.slice(-1)[0];
      if (lastProcess) {
        console.log(`${step.tool}: ${lastProcess.status} ${lastProcess.txHash || ''}`);
      }
    }
  },
});

console.log('Done!', result);

Timing and Latency

Cross-chain Composer transactions take longer than same-chain because they include a bridge transfer. Actual duration depends on the bridge selected, network congestion, and the number of steps in the route. There are no fixed values. You can influence bridge selection with the order parameter:
const quote = await getQuote({
  // ... other params
  options: {
    order: 'FASTEST',  // Prioritise speed over cost
  },
});
The route response includes an estimate.executionDuration field (in seconds) that reflects the expected end-to-end time for the chosen path. Use this to set user expectations in your UI. For controlling how quickly the API returns quotes, see API Latency & Optimization.

Handling Partial Failures

Cross-chain operations have two phases. Each phase is atomic within its chain, but the overall flow is eventually consistent.
ScenarioWhat HappensUser Impact
Source chain tx failsTransaction reverts, no funds leaveNo impact, user can retry
Bridge failsDepends on bridge, most auto-refundUser receives refund on source chain
Destination Composer failsBridged tokens arrive but deposit failsUser has tokens on destination chain (not deposited)

Monitoring for Partial Completion

Check the status response for partial completion:
if (status.status === 'DONE' && status.substatus === 'PARTIAL') {
  console.log('Bridge succeeded but destination action may have partially completed.');
  console.log('Check destination chain for received tokens.');
}

if (status.substatus === 'REFUND_IN_PROGRESS') {
  console.log('Refund is being processed on the source chain.');
}
For the full list of status and substatus values, see Transaction Status Tracking.

Best Practices

  1. Always poll status — Don’t assume completion after the source chain tx confirms
  2. Set reasonable slippage — Cross-chain routes involve more steps; consider 0.005 (0.5%) or higher
  3. Use FASTEST for time-sensitive operations — The order parameter controls bridge selection
  4. Handle all terminal states — Check for DONE, FAILED, and PARTIAL substatus
  5. Show progress to users — Use the status polling or SDK hooks to display bridge progress

Next Steps