Skip to main content

Overview

This guide shows how to use Magic’s Embedded Wallet with the Li.Fi API to swap tokens on Base. Li.Fi aggregates DEXs and bridges, finding the best rate across multiple protocols. Your app fetches a quote, handles token approval, and executes the swap — all client-side through the user’s Embedded Wallet.

Prerequisites

Before starting, ensure you have:
  1. A Magic Publishable API Key from your Magic Dashboard
  2. A Base RPC endpoint (e.g., from Alchemy or QuickNode)
  3. Tokens on Base in the user’s wallet (ETH for gas + the token to swap)

How It Works

  1. User authenticates with Magic
  2. Your app fetches a swap quote from Li.Fi’s /v1/quote endpoint
  3. If swapping an ERC-20 token, the user approves Li.Fi’s contract to spend their tokens
  4. Your app sends the swap transaction using the transactionRequest from the quote
  5. Li.Fi routes the swap through the best available DEX

Setting Up the Client

Initialize Magic and create viem clients connected to Base.
TypeScript
import { Magic } from "magic-sdk";
import { createWalletClient, createPublicClient, custom, http } from "viem";
import { base } from "viem/chains";

const magic = new Magic("YOUR_PUBLISHABLE_KEY", {
  network: {
    rpcUrl: "https://base-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY",
    chainId: 8453,
  },
});

const publicClient = createPublicClient({
  chain: base,
  transport: http("https://base-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY"),
});

const walletClient = createWalletClient({
  chain: base,
  transport: custom(magic.rpcProvider),
});

const [userAddress] = await walletClient.getAddresses();

Getting a Swap Quote

Fetch a quote from Li.Fi to find the best swap route. No API key is required.
TypeScript
const LIFI_API = "https://li.quest/v1";

interface QuoteResponse {
  estimate: {
    toAmount: string;
    toAmountMin: string;
    approvalAddress: string;
    gasCosts: { estimate: string; token: { symbol: string } }[];
  };
  transactionRequest: {
    from: string;
    to: string;
    data: string;
    value: string;
    gasLimit: string;
    gasPrice: string;
    chainId: number;
  };
}

async function getQuote(
  fromToken: string,
  toToken: string,
  amount: string,
  fromAddress: string,
): Promise<QuoteResponse> {
  const params = new URLSearchParams({
    fromChain: "8453",
    toChain: "8453",
    fromToken,
    toToken,
    fromAmount: amount,
    fromAddress,
    slippage: "0.005",
  });

  const res = await fetch(`${LIFI_API}/quote?${params}`);
  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.message || `Quote failed: ${res.status}`);
  }
  return res.json();
}
The slippage parameter is a decimal — 0.005 means 0.5%. Li.Fi returns toAmountMin which accounts for slippage, so users know the minimum they’ll receive.

Token Approval

When swapping an ERC-20 token (not native ETH), the user must approve Li.Fi’s contract to spend their tokens. The approval address comes from the quote response.
TypeScript
import { type Address, parseUnits, erc20Abi } from "viem";

async function checkAndApprove(
  tokenAddress: Address,
  spender: Address,
  amount: bigint,
) {
  // Check current allowance
  const allowance = await publicClient.readContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "allowance",
    args: [userAddress, spender],
  });

  if (allowance >= amount) return; // Already approved

  // Approve the exact amount
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "approve",
    args: [spender, amount],
    account: userAddress,
  });

  await publicClient.waitForTransactionReceipt({ hash });
}

Executing the Swap

Combine the quote, approval, and transaction execution into a single flow.
TypeScript
// Native ETH is represented by the zero address in Li.Fi
const NATIVE_TOKEN = "0x0000000000000000000000000000000000000000";
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const USDC_DECIMALS = 6;

async function swapTokens(
  fromToken: Address,
  toToken: Address,
  amount: string,
  decimals: number,
) {
  const fromAmount = parseUnits(amount, decimals).toString();

  // 1. Get quote
  const quote = await getQuote(fromToken, toToken, fromAmount, userAddress);

  // 2. Approve if swapping an ERC-20 (not native ETH)
  if (fromToken !== NATIVE_TOKEN) {
    await checkAndApprove(
      fromToken,
      quote.estimate.approvalAddress as Address,
      BigInt(fromAmount),
    );
  }

  // 3. Execute the swap
  const hash = await walletClient.sendTransaction({
    to: quote.transactionRequest.to as Address,
    data: quote.transactionRequest.data as `0x${string}`,
    value: BigInt(quote.transactionRequest.value),
    gas: BigInt(quote.transactionRequest.gasLimit),
    account: userAddress,
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  return {
    hash: receipt.transactionHash,
    toAmount: quote.estimate.toAmount,
    toAmountMin: quote.estimate.toAmountMin,
  };
}

Example: Swap ETH for USDC

TypeScript
// Swap 0.01 ETH for USDC on Base
const result = await swapTokens(
  NATIVE_TOKEN,          // from: native ETH
  USDC_ADDRESS,          // to: USDC
  "0.01",                // amount
  18,                    // ETH has 18 decimals
);

console.log(`Swap tx: ${result.hash}`);

Example: Swap USDC for ETH

TypeScript
// Swap 10 USDC for ETH on Base
const result = await swapTokens(
  USDC_ADDRESS,          // from: USDC
  NATIVE_TOKEN,          // to: native ETH
  "10",                  // amount
  USDC_DECIMALS,         // USDC has 6 decimals
);

console.log(`Swap tx: ${result.hash}`);
When swapping USDC for ETH, the approval step runs automatically. When swapping ETH for USDC, no approval is needed since ETH is the native token.

Token Addresses

Common tokens on Base (chain ID 8453):
TypeScript
// Native ETH (use zero address in Li.Fi)
const ETH = "0x0000000000000000000000000000000000000000";

// USDC on Base (native, issued by Circle)
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";

// DAI on Base
const DAI = "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb";
Li.Fi also accepts token symbols (e.g., "ETH", "USDC") instead of addresses, but using addresses is more reliable to avoid ambiguity.

Key Dependencies

PackagePurpose
magic-sdkMagic authentication and Embedded Wallet
viemEthereum client for contract interactions
No additional SDK is needed — Li.Fi’s REST API is called directly via fetch.

Troubleshooting

Symptoms: The /quote endpoint returns an error or empty result.Solutions:
  • Verify the token addresses are correct for Base (chain ID 8453)
  • Check that fromAmount is in the smallest unit (e.g., wei for ETH, 6 decimals for USDC)
  • Ensure the amount is large enough — very small swaps may not have viable routes
  • Try increasing slippage if the market is volatile
Symptoms: The transaction is sent but reverts on-chain.Solutions:
  • Check that the token approval was confirmed before sending the swap
  • The quote may have expired — quotes are only valid for a short time, so fetch a new one and retry
  • Ensure the user has enough ETH for gas fees on Base
Symptoms: The ERC-20 approval transaction fails.Solutions:
  • Verify the user holds the token they’re trying to swap
  • Check that the approvalAddress from the quote is being used (not the swap contract)
  • Ensure the user has ETH for the approval gas fee
Symptoms: The output amount is lower than the quoted estimate.Solutions:
  • This is normal within the slippage tolerance — check toAmountMin for the guaranteed minimum
  • Reduce slippage in the quote request for tighter control (but too low may cause failures)
  • For large swaps, consider splitting into smaller transactions

Resources

Magic Embedded Wallets

Learn about Magic’s Embedded Wallet product

Li.Fi Documentation

Official Li.Fi API and SDK documentation

EVM Transaction Signing

Guide for signing transactions with Magic wallets

Base Documentation

Official Base network documentation