Building Multichain DeFi with LI.FI
The multichain problem in DeFi is real and painful. If you want to move USDC from Polygon into a Morpho vault on Base while also staking some ETH into Lido on mainnet - without LI.FI - you're looking at half a dozen separate protocol integrations, gas on three chains, and a user experience that drops 80% of people before they finish.
LI.FI solves this as infrastructure. It's a cross-chain routing and execution layer that unifies bridges, DEXs, solvers, and yield protocols behind a single API. As of 2026, it powers over 600 partners including MetaMask, Phantom, and Robinhood Wallet - processing $30B+ in lifetime volume.
This post is a developer-focused breakdown of the three API surfaces I find most interesting: the Swap/Crosschain API, Composer, and Earn. I'll cover what each one does, where it shines, and close with a full multichain yield optimizer pattern that combines all three.
The Stack at a Glance
Before diving in, here's how the three products relate to each other:
| Product | Base URL | Core Job |
|---|---|---|
| Swap / Crosschain API | https://li.quest/v1 | Route & execute any-to-any token swaps across chains |
| Composer | https://li.quest/v1 | Bundle swap + bridge + deposit into one transaction |
| Earn Data API | https://earn.li.fi/v1 | Discover & track yield vaults across 20+ protocols |
Composer and Earn are built on top of the core Swap API - they extend the same /quote endpoint. Earn adds vault discovery and portfolio tracking as a separate data layer at earn.li.fi. Understanding this layering makes the whole system click.
Part 1 - The Swap & Crosschain API
What It Does
The core LI.FI API (https://li.quest/v1) is a REST interface for requesting optimal routes for token swaps - same-chain or cross-chain. It queries DEX aggregators (1inch, etc.), direct DEXs (Uniswap, etc.), bridges (Stargate, Across, etc.), and intent-based solver networks simultaneously, then returns the best path by price, speed, or security preference.
No API key is required by default. Keys are only needed for higher rate limits, passed via the x-lifi-api-key header.
Core Endpoints
GET /v1/quote → Best single route for a given swap
GET /v1/routes → All available routes (user picks)
GET /v1/status → Track execution status by txHash
GET /v1/chains → All supported chains + metadata
GET /v1/tokens → Supported tokens with metadata
GET /v1/tools → Available bridges & DEXs per chain
GET /v1/connections → Valid from/to chain+token pairs
Getting a Crosschain Quote
The /quote endpoint is the workhorse. Here's how to request a route for swapping ETH on Arbitrum to ETH on Optimism:
// Using the LI.FI TypeScript SDK
import { createConfig, getQuote, ChainId } from '@lifi/sdk'
createConfig({ integrator: 'my-app' })
const quote = await getQuote({
fromAddress: '0xYourAddress',
fromChain: ChainId.ARB, // 42161
toChain: ChainId.OPT, // 10
fromToken: '0x0000000000000000000000000000000000000000', // ETH
toToken: '0x0000000000000000000000000000000000000000', // ETH
fromAmount: '1000000000000000000', // 1 ETH in wei
})
console.log(quote.estimate.toAmount) // expected output amount
console.log(quote.estimate.executionDuration) // ms to complete
console.log(quote.estimate.gasCosts) // gas breakdown
Or directly via REST without the SDK:
const params = new URLSearchParams({
fromChain: '42161',
toChain: '10',
fromToken: '0x0000000000000000000000000000000000000000',
toToken: '0x0000000000000000000000000000000000000000',
fromAmount: '1000000000000000000',
fromAddress: '0xYourAddress',
})
const quote = await fetch(`https://li.quest/v1/quote?${params}`)
.then(r => r.json())
The response includes the full route, tool used (bridge/DEX name), estimated output, gas costs, and a transactionRequest object ready to be signed and sent.
Tracking a Transaction
After execution, poll status with the transaction hash:
// GET /v1/status?txHash=0x...&fromChain=42161&toChain=10
const status = await fetch(
`https://li.quest/v1/status?txHash=${txHash}&fromChain=42161&toChain=10`
).then(r => r.json())
// status.status: 'PENDING' | 'DONE' | 'FAILED' | 'INVALID' | 'NOT_FOUND'
The SDK handles this automatically through the executeRoute function's hooks if you want managed execution.
Use Cases
Multichain wallet swap tab - One integration, 60+ chains. Users pick tokens and chains, you call /quote, display the route and estimated output, then execute. No per-bridge SDK needed.
Cross-chain DCA bot - Call /quote on a schedule, execute when conditions are met (e.g., price threshold). The SDK handles signing and status tracking.
Smart rebalancer - Given a portfolio spread across chains, fetch quotes for all required moves, compare total gas costs, execute the most efficient path.
Fiat onramp bridge - Pair with a fiat provider: user buys USDC on Ethereum via card, LI.FI routes it to Arbitrum for cheaper DeFi interactions in the next step.
Part 2 - Composer
The Problem It Solves
Most DeFi actions aren't atomic. Depositing into a cross-chain vault without Composer looks like this:
1. Approve USDC on Polygon → sign
2. Swap USDC → USDC.e on Polygon → sign + wait
3. Bridge USDC.e to Base → sign + wait (minutes)
4. Approve USDC on Base → sign
5. Deposit into Morpho vault on Base → sign
Five transactions, four wallet signatures, cross-chain waiting, and failure on step 4 leaves funds stranded mid-route.
Composer compresses all of this into one transaction, one signature.
How It Actually Works
Composer is built on two components:
- Onchain VM (Execution Engine) - A smart contract that can call any sequence of other onchain protocols. It handles the full execution without requiring users to understand each step.
- eDSL and Compiler - A TypeScript domain-specific language that expresses contract interactions, compiled to bytecode for the VM.
The key insight: when you call /v1/quote and set toToken to a vault token address (an LP token, a receipt token from a lending protocol, an LST), Composer activates automatically. No extra parameters. No separate endpoint. LI.FI's routing engine detects the vault token, identifies the execution path, compiles the Composer instructions, and simulates the full execution path before returning - so you know it'll succeed before the user signs anything.
Dynamic calldata injection means the exact output of step 1 (post-swap amount) is automatically passed into step 2 as input - no manual estimation, no failed transactions from rounding.
Same-chain compositions are fully atomic: all steps succeed or none do. Cross-chain flows are sequential across the bridge boundary but handled as a single user action.
Activating Composer - It's Just a Quote
Composer is not a separate API. You use the exact same /v1/quote endpoint:
// Regular swap quote (no Composer)
// toToken = regular ERC-20 like USDC
// → LI.FI routes through DEXs and bridges
// Composer deposit quote (Composer auto-activates)
// toToken = vault token (e.g., Morpho vault share token)
// → LI.FI routes swap + bridge + deposit in one tx
Here's a complete Composer deposit example using the SDK:
import { createConfig, getQuote, executeRoute } from '@lifi/sdk'
createConfig({ integrator: 'my-yield-app' })
// Deposit 100 USDC (on Base) into a Morpho USDC vault (on Base)
// toToken is the Morpho vault's LP/share token address
const quote = await getQuote({
fromAddress: '0xYourWalletAddress',
fromChain: 8453, // Base
toChain: 8453, // Base (same-chain = fully atomic)
fromToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
toToken: '0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A', // Morpho vault token
fromAmount: '100000000', // 100 USDC (6 decimals)
})
// quote.includedSteps shows you every action that will execute
// e.g. [{ type: 'swap' }, { type: 'protocol' }]
const result = await executeRoute(quote, {
updateRouteHook: (route) => {
const active = route.steps.find(s => s.execution?.status === 'PENDING')
if (active) console.log(`executing: ${active.type}`)
},
})
Cross-chain works the same way - just change fromChain. Composer handles the bridge step internally.
Curl Example - One Line
# Deposit 1 USDC into a Morpho vault on Base - Composer activates automatically
curl -X GET 'https://li.quest/v1/quote?\
fromChain=8453&\
toChain=8453&\
fromToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&\
toToken=0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A&\
fromAddress=YOUR_WALLET_ADDRESS&\
toAddress=YOUR_WALLET_ADDRESS&\
fromAmount=1000000'
Set toToken to any supported vault token. Composer activates. No extra config.
Supported Protocols (April 2026)
| Protocol | Type | Actions |
|---|---|---|
| Morpho V1 & V2 | Lending / Vaults | Deposit, Withdraw |
| Aave V3 | Lending | Deposit, Withdraw |
| Euler | Lending | Deposit, Withdraw |
| Pendle | Yield | Deposit, Withdraw |
| Lido wstETH | Liquid Staking | Deposit, Withdraw |
| EtherFi | Liquid Staking | Deposit, Withdraw |
| Ethena | Yield | Deposit |
| Maple | Lending | Deposit |
| Seamless | Lending | Deposit, Withdraw |
| HyperLend | Lending | Deposit, Withdraw |
| Kinetiq | Staking | Deposit |
Current limitations: EVM chains only (no Solana yet). Targets must return a tokenized position - vault shares, LP tokens, or LSTs.
Use Cases
One-click vault entry - User has any token on any chain. They pick a vault. Composer finds the path: bridge if needed, swap to the right asset, deposit. One signature.
Liquidity migration - Move funds from Aave on Ethereum to Morpho on Base atomically. Withdraw → bridge → deposit is a single user action.
Bridge-and-stake - Send ETH cross-chain and stake into Lido or EtherFi in one transaction. Common pattern for liquid staking front-ends.
Protocol onboarding campaigns - "Deposit into our new vault for rewards" becomes a one-click CTA instead of a five-step tutorial.
Part 3 - LI.FI Earn
What It Is
Earn is LI.FI's most recent product expansion, applying the same aggregation architecture to on-chain yield. It's two layers working together:
- Earn Data API (
https://earn.li.fi/v1) - vault discovery, standardized metadata, APY/TVL analytics, and portfolio position tracking across 20+ protocols and 60+ chains. - Composer - execution. The same Composer from Part 2, invoked once a vault is selected.
The data problem Earn solves is real: Aave's APY format, Morpho's vault structure, Pendle's yield mechanics, and Ethena's model are all completely different. Earn normalizes everything into a NormalizedVault schema - one consistent object regardless of the underlying protocol. Compare APYs without writing a custom data adapter for each one.
Earn Data API Endpoints
GET /v1/earn/chains → Chains with active vaults
GET /v1/earn/protocols → Supported protocols list
GET /v1/earn/vaults → List vaults (filter, sort, paginate)
GET /v1/earn/vaults/:chainId/:vaultAddress → Single vault detail
GET /v1/earn/portfolio/:walletAddress/positions → User\'s active positions
All Earn Data API calls require an API key from the LI.FI Partner Portal, passed via x-lifi-api-key.
Vault Discovery
// Top 5 USDC vaults on Base, sorted by APY
const res = await fetch(
'https://earn.li.fi/v1/earn/vaults?' +
new URLSearchParams({
chainId: '8453',
asset: 'USDC',
sortBy: 'apy',
limit: '5',
}),
{ headers: { 'x-lifi-api-key': process.env.LIFI_API_KEY! } }
)
const { vaults } = await res.json()
// Each vault follows the NormalizedVault schema:
// { chainId, address, protocol, asset, apy, tvlUsd, underlyingTokens, ... }
const best = vaults[0]
console.log(`${best.protocol}: ${best.apy}% APY, $${best.tvlUsd} TVL`)
Portfolio Tracking
// All of a user's DeFi positions across supported protocols
const portfolio = await fetch(
`https://earn.li.fi/v1/earn/portfolio/0xYourAddress/positions`,
{ headers: { 'x-lifi-api-key': process.env.LIFI_API_KEY! } }
).then(r => r.json())
// portfolio.positions[]:
// { chainId, protocolName, asset, balanceUsd, balanceNative }
Deposit via Composer (the handoff)
Once you have a vault from the Earn Data API, you execute by passing the vault's contract address as toToken to the standard /v1/quote endpoint:
// vaultAddress + vaultChainId come from Earn Data API
const { address: vaultAddress, chainId: vaultChainId } = best
const depositQuote = await fetch(
`https://li.quest/v1/quote?` +
new URLSearchParams({
fromChain: String(userChainId),
toChain: String(vaultChainId),
fromToken: userToken,
toToken: vaultAddress, // ← vault token activates Composer
fromAmount: userAmount,
fromAddress: userWallet,
toAddress: userWallet,
})
).then(r => r.json())
// Execute the quote via SDK - one signature
await executeRoute(depositQuote)
Use Cases
Earn tab in a wallet - Integrate Earn to surface yield opportunities without knowing anything about Aave or Morpho. The API handles data; Composer handles execution.
Yield comparison dashboard - Live APYs across chains and protocols in a normalized table. No per-protocol parsers.
Portfolio tracker - Every DeFi position across all supported protocols in one API call.
AI agent treasury management - Earn ships with an MCP integration. Agents discover strategies, compare yields, and deposit programmatically.
DAO treasury optimizer - Route idle stablecoin reserves to best-in-class yield without a governance vote per protocol.
Putting It Together: A Multichain Yield Optimizer
Here's the full pattern combining all three surfaces. This Nuxt composable finds the best yield opportunity for a given token, then executes the cross-chain deposit in one user action.
Architecture
User: token + amount + source chain
│
▼
[Earn Data API] /v1/earn/vaults?asset=USDC&sortBy=apy
→ NormalizedVault[] sorted by APY
│
▼
Filter: TVL floor + pick vault[0]
│
▼
[Composer] li.quest/v1/quote
fromToken = user token, fromChain = user chain
toToken = vault.address (triggers Composer)
→ full swap + bridge + deposit path, pre-simulated
│
▼
[SDK executeRoute] One wallet signature
│
▼
[Earn Data API] /v1/earn/portfolio/:address/positions
→ Confirm position is active
Core Logic
// lib/yieldOptimizer.ts
import { createConfig, getQuote, executeRoute } from '@lifi/sdk'
createConfig({ integrator: 'yield-optimizer' })
const EARN_API = 'https://earn.li.fi/v1'
const API_KEY = process.env.LIFI_API_KEY!
interface NormalizedVault {
id: string
address: string
protocol: string
chainId: number
apy: number
tvlUsd: number
}
export interface OptimizerParams {
walletAddress: string
fromChainId: number
fromToken: string // ERC-20 address
fromAmount: string // smallest unit (wei)
assetSymbol: string // e.g. 'USDC'
minTvlUsd?: number // safety floor, default $1M
}
// 1. Find best vault via Earn Data API
async function findBestVault(asset: string, minTvlUsd = 1_000_000) {
const res = await fetch(
`${EARN_API}/earn/vaults?` +
new URLSearchParams({ asset, sortBy: 'apy', limit: '20' }),
{ headers: { 'x-lifi-api-key': API_KEY } }
)
const { vaults }: { vaults: NormalizedVault[] } = await res.json()
const safe = vaults.filter(v => v.tvlUsd >= minTvlUsd)
if (!safe.length) throw new Error('No vaults meet TVL threshold')
return safe[0]
}
// 2. Get Composer quote - vault token as toToken activates Composer
async function getComposerQuote(params: OptimizerParams, vault: NormalizedVault) {
return getQuote({
fromAddress: params.walletAddress,
fromChain: params.fromChainId,
toChain: vault.chainId,
fromToken: params.fromToken,
toToken: vault.address,
fromAmount: params.fromAmount,
})
}
// 3. Execute - one wallet signature
async function execute(quote: Awaited<ReturnType<typeof getComposerQuote>>) {
return executeRoute(quote, {
updateRouteHook: (r) => {
const active = r.steps.find(s => s.execution?.status === 'PENDING')
if (active) console.log(`[optimizer] ${active.type}`)
},
})
}
// 4. Confirm position
async function getPositions(wallet: string) {
return fetch(
`${EARN_API}/earn/portfolio/${wallet}/positions`,
{ headers: { 'x-lifi-api-key': API_KEY } }
).then(r => r.json())
}
// ─── Orchestrator ─────────────────────────────────────────────────────────
export async function optimizeYield(params: OptimizerParams) {
const vault = await findBestVault(params.assetSymbol, params.minTvlUsd)
const quote = await getComposerQuote(params, vault)
const result = await execute(quote)
const txHash = result.steps.at(-1)?.execution?.process.at(-1)?.txHash ?? ''
const positions = await getPositions(params.walletAddress)
return { vault, txHash, positions }
}
Nuxt Composable + Page
// composables/useYieldOptimizer.ts
import { optimizeYield, type OptimizerParams } from '~/lib/yieldOptimizer'
export function useYieldOptimizer() {
const result = ref<Awaited<ReturnType<typeof optimizeYield>> | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function run(params: OptimizerParams) {
loading.value = true
error.value = null
try {
result.value = await optimizeYield(params)
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
return { result, loading, error, run }
}
<!-- pages/optimize.vue -->
<script setup lang="ts">
const { result, loading, error, run } = useYieldOptimizer()
// assumes you have a wallet composable providing address
const address = '0xYourConnectedWallet'
async function optimize() {
await run({
walletAddress: address,
fromChainId: 137,
fromToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon
fromAmount: '1000000000', // 1000 USDC
assetSymbol: 'USDC',
minTvlUsd: 1_000_000,
})
}
</script>
<template>
<div>
<button :disabled="loading" @click="optimize">
{{ loading ? 'Scanning yield...' : 'Optimize & Deposit' }}
</button>
<p v-if="error" class="text-red-500">{{ error }}</p>
<div v-if="result">
<p>Protocol: <strong>{{ result.vault.protocol }}</strong></p>
<p>APY: {{ result.vault.apy }}%</p>
<p>Chain: {{ result.vault.chainId }}</p>
<p>TX: <code>{{ result.txHash }}</code></p>
</div>
</div>
</template>
Setup & Auth Reference
npm install @lifi/sdk
# or
pnpm add @lifi/sdk
// plugins/lifi.client.ts
import { createConfig } from '@lifi/sdk'
export default defineNuxtPlugin(() => {
createConfig({ integrator: 'your-app-name' })
})
| Surface | Base URL | Auth |
|---|---|---|
| Swap / Quote / Composer | https://li.quest/v1 | No key required (key for higher limits) |
| Earn Data API | https://earn.li.fi/v1 | API key required (x-lifi-api-key) |
API keys are from the LI.FI Partner Portal. Keep them server-side - never in client-side code or the Widget.
Decision Map
| What you're building | Use |
|---|---|
| Swap / bridge UI | Swap API - GET /v1/quote |
| Enter a vault from any chain, one click | Composer - pass vault token as toToken |
| Yield discovery, APY comparison | Earn Data API - earn.li.fi/v1/earn/vaults |
| Portfolio view | Earn Data API - /portfolio/:address/positions |
| Full yield optimizer | Earn Data API + Composer |
| AI agent / autonomous treasury | All three + MCP Server |
API Developer Experience Feedback
Building with the LI.FI stack is incredibly powerful, but there are a few areas where the developer experience could be tightened:
- The "Magic" of Composer Can Be Tricky: Activating Composer implicitly via the
toTokenaddress is elegant, but it can be hard to debug if a route isn't recognized as a vault. A dedicated flag in the SDK/API to strictly enforce a Composer execution path would help catch errors early. - Earn Data API Historical APY: The normalized schema is fantastic for current stats, but building rich yield dashboards often requires historical APY charts. Adding a
/earn/historicalendpoint in the future would eliminate the need for a separate data provider. - Execution State Tracking: While polling the
/statusendpoint works, offering webhook support for cross-chain transaction status changes would be a massive upgrade. It would entirely prevent frontends from needing to aggressively poll for minutes during slower bridge operations.
Closing Thoughts
What I like most about LI.FI's architecture is how Composer is designed: rather than a new API surface, it's a behavior that activates based on the destination token. That means you get one-transaction DeFi orchestration without changing your integration - just pass a vault token address instead of a regular token. Earn extends the same philosophy to data: one schema, 20+ protocols, no adapter code.
The combination of these three layers means you can build real DeFi products - yield optimizers, cross-chain dashboards, one-click vault UIs - with far less surface area to maintain than integrating protocols individually. That's the actual value proposition.