Skip to main content
This quickstart walks you through a same-chain Composer transaction: depositing USDC into a Morpho vault on Base. By the end, you’ll understand the full Composer flow: quote, approve, execute, track.

Prerequisites

  • A wallet address with USDC on Base (even a small amount like 1 USDC works)
  • Node.js 18+ (for the TypeScript examples) or curl

1

Get a Composer quote

Request a quote with toToken set to the vault token address:
curl -X GET 'https://li.quest/v1/quote?\
fromChain=8453&\
toChain=8453&\
fromToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&\
toToken=0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A&\
fromAddress=0xYOUR_WALLET_ADDRESS&\
toAddress=0xYOUR_WALLET_ADDRESS&\
fromAmount=1000000'
Key parameters:
ParameterValueDescription
fromChain8453Base chain ID
toChain8453Base (same-chain deposit)
fromToken0x8335...2913USDC on Base
toToken0x7BfA...34AMorpho vault token address
fromAmount10000001 USDC (6 decimals)
The toToken is always the vault token address of the target protocol. You can find vault token addresses on the protocol’s own app or documentation.
The response includes transactionRequest, a ready-to-sign EVM transaction, along with estimated output amounts and the tools used.
2

Set token allowance

Before executing, ensure the LI.FI contract is approved to spend your tokens. The approval address is returned in the quote response at quote.estimate.approvalAddress.
If you’re sending a native token (e.g., ETH), skip this step. Native tokens don’t require approval.
import { ethers } from 'ethers';

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)',
];

const checkAndSetAllowance = async (
  signer: ethers.Signer,
  tokenAddress: string,
  approvalAddress: string,
  amount: string
) => {
  const erc20 = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
  const signerAddress = await signer.getAddress();
  const allowance = await erc20.allowance(signerAddress, approvalAddress);

  if (allowance.lt(amount)) {
    const tx = await erc20.approve(approvalAddress, amount);
    await tx.wait();
    console.log('Approval set.');
  } else {
    console.log('Allowance already sufficient.');
  }
};

await checkAndSetAllowance(
  signer,
  quote.action.fromToken.address,
  quote.estimate.approvalAddress,
  quote.action.fromAmount
);
3

Execute the transaction

Send the transaction using the transactionRequest object from the quote response.
const tx = await signer.sendTransaction(quote.transactionRequest);
console.log('Transaction sent:', tx.hash);

const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt.transactionHash);
That’s it. Composer handles the swap and deposit in a single atomic transaction.
4

Track the status

For same-chain transactions, the transaction is complete once confirmed. For cross-chain Composer flows, poll the /status endpoint:
const getStatus = async (txHash: string, fromChain: number, toChain: number) => {
  const result = await axios.get(`${API_URL}/status`, {
    params: { txHash, fromChain, toChain },
  });
  return result.data;
};

// For cross-chain transfers, poll until complete
if (quote.action.fromChainId !== quote.action.toChainId) {
  let status;
  do {
    status = await getStatus(tx.hash, quote.action.fromChainId, quote.action.toChainId);
    console.log('Status:', status.status, status.substatus);

    if (status.status !== 'DONE' && status.status !== 'FAILED') {
      await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5s
    }
  } while (status.status !== 'DONE' && status.status !== 'FAILED');

  console.log('Final status:', status.status);
}
StatusMeaning
NOT_FOUNDTransaction doesn’t exist or not yet mined
INVALIDHash is not tied to the requested tool
PENDINGTransaction is in progress
DONECompleted successfully
FAILEDTransaction failed
For the full status reference, see Transaction Status Tracking.

Full Working Example

Copy-paste this complete example to run your first Composer transaction:
import { ethers } from 'ethers';
import axios from 'axios';

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

// --- Configuration ---
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY';
const RPC_URL = 'https://mainnet.base.org';
const FROM_CHAIN = 8453;                                              // Base
const FROM_TOKEN = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';     // USDC on Base
const TO_TOKEN = '0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A';       // Morpho vault token
const FROM_AMOUNT = '1000000';                                        // 1 USDC

// --- Setup ---
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);

// --- Helpers ---
const getQuote = async (fromAddress: string) => {
  const result = await axios.get(`${API_URL}/quote`, {
    params: {
      fromChain: FROM_CHAIN,
      toChain: FROM_CHAIN,
      fromToken: FROM_TOKEN,
      toToken: TO_TOKEN,
      fromAmount: FROM_AMOUNT,
      fromAddress,
      toAddress: fromAddress,
    },
  });
  return result.data;
};

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)',
];

const ensureAllowance = async (
  tokenAddress: string,
  approvalAddress: string,
  amount: string
) => {
  const erc20 = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
  const address = await signer.getAddress();
  const allowance = await erc20.allowance(address, approvalAddress);

  if (allowance < BigInt(amount)) {
    console.log('Setting allowance...');
    const tx = await erc20.approve(approvalAddress, amount);
    await tx.wait();
    console.log('Allowance set.');
  }
};

// --- Main ---
const run = async () => {
  const address = await signer.getAddress();
  console.log('Wallet:', address);

  // 1. Get quote
  console.log('Requesting Composer quote...');
  const quote = await getQuote(address);
  console.log('Quote received. Tool:', quote.tool);
  console.log('Estimated output:', quote.estimate.toAmount);

  // 2. Approve
  await ensureAllowance(
    quote.action.fromToken.address,
    quote.estimate.approvalAddress,
    quote.action.fromAmount
  );

  // 3. Execute
  console.log('Sending transaction...');
  const tx = await signer.sendTransaction(quote.transactionRequest);
  console.log('Tx hash:', tx.hash);

  const receipt = await tx.wait();
  console.log('Confirmed in block:', receipt.blockNumber);
  console.log('Done! USDC deposited into Morpho vault.');
};

run().catch(console.error);

What Just Happened?

Behind the scenes, Composer:
  1. Identified the optimal path — LI.FI’s routing engine determined the best way to convert USDC into the Morpho vault token
  2. Compiled eDSL instructions — The Composer compiler generated bytecode for the onchain VM
  3. Simulated the execution — The full path was simulated before returning the quote, ensuring it will succeed
  4. Executed atomically — Your single transaction swapped USDC and deposited into Morpho in one atomic operation

Next Steps