Skip to main content
This guide walks you through building a Next.js application that lets users sign in with email, get a smart wallet, and bridge USDC from Base to Polygon using LI.FI.

Prerequisites

  • A Crossmint production client-side API key with wallet permissions enabled (create in Console → API Keys)
  • USDC on Base mainnet in your Crossmint wallet
  • ETH on Base for gas fees (not required if gas sponsorship is enabled)
  • Node.js 18+

How It Works

  1. The user signs in with email via Crossmint Auth
  2. Crossmint provisions an EVM smart wallet
  3. The app fetches a bridge quote from the LI.FI API (server-side)
  4. The user approves the USDC spend and signs the swap transaction (client-side)
  5. The app polls LI.FI for the bridge status until completion

Step-by-Step

1

Create the Project

Scaffold a new Next.js app:
npx create-next-app@latest swap-lifi --typescript --tailwind
cd swap-lifi
Install the Crossmint SDK and viem:
npm i @crossmint/client-sdk-react-ui
npm install viem
Create a .env.local file in the project root:
.env.local
NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY=<your-client-api-key>
Replace <your-client-api-key> with the client-side API key from the Crossmint Console.
2

Set Up Crossmint Providers

Wrap the app in Crossmint’s auth and wallet providers. This configures wallet creation on Base with an email signer, giving users a non-custodial wallet they fully control.
app/providers.tsx
"use client";

import {
    CrossmintProvider,
    CrossmintAuthProvider,
    CrossmintWalletProvider,
} from "@crossmint/client-sdk-react-ui";

export function Providers({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <CrossmintProvider
            apiKey={
                process.env
                    .NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY!
            }
        >
            <CrossmintAuthProvider>
                <CrossmintWalletProvider
                    createOnLogin={{
                        chain: "base",
                        signer: { type: "email" },
                    }}
                >
                    {children}
                </CrossmintWalletProvider>
            </CrossmintAuthProvider>
        </CrossmintProvider>
    );
}
Then import this in your root layout:
app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="en">
            <body>
                <Providers>{children}</Providers>
            </body>
        </html>
    );
}
3

Create the LI.FI Server Actions

These Next.js Server Actions call the LI.FI API to fetch quotes and poll the bridge status.
app/actions/swap.ts
"use server";

const LIFI_API = "https://li.quest/v1";

export async function getSwapQuote(params: {
    walletAddress: string;
    fromChainId: string;
    toChainId: string;
    fromToken: string;
    toToken: string;
    fromAmount: string;
}) {
    const res = await fetch(
        `${LIFI_API}/quote?` +
            new URLSearchParams({
                fromChain: params.fromChainId,
                toChain: params.toChainId,
                fromToken: params.fromToken,
                toToken: params.toToken,
                fromAmount: params.fromAmount,
                fromAddress: params.walletAddress,
                integrator: "crossmint",
            })
    );

    if (!res.ok) {
        return { error: await res.text() };
    }

    return res.json();
}

export async function getSwapStatus(
    txHash: string,
    fromChainId: string,
    toChainId: string
) {
    const res = await fetch(
        `${LIFI_API}/status?` +
            new URLSearchParams({
                txHash,
                fromChain: fromChainId,
                toChain: toChainId,
            })
    );

    return res.json();
}
getSwapQuote returns a quote object that includes the transaction calldata and an estimated output amount. getSwapStatus polls the bridge status by transaction hash.
4

Build the Swap Page

The main page handles three responsibilities: authentication, fetching a quote, and executing the swap. The swap flow has two on-chain transactions:
  1. Approve — grant the LI.FI router permission to spend USDC
  2. Swap — execute the bridge transaction using calldata from the quote
Because the wallet uses an email signer (non-custodial), a signing prompt appears the first time the user sends a transaction in a session. Subsequent transactions in the same session do not require additional approval.
app/page.tsx
"use client";

import {
    useAuth,
    useWallet,
    EVMWallet,
} from "@crossmint/client-sdk-react-ui";
import { useState } from "react";
import {
    encodeFunctionData,
    erc20Abi,
    formatUnits,
    parseUnits,
} from "viem";
import {
    getSwapQuote,
    getSwapStatus,
} from "./actions/swap";

const FROM_CHAIN = "8453"; // Base
const TO_CHAIN = "137"; // Polygon
const FROM_TOKEN =
    "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const TO_TOKEN =
    "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // USDC on Polygon
const USDC_DECIMALS = 6;

export default function Home() {
    const { login, status } = useAuth();
    const { wallet } = useWallet();

    const [amount, setAmount] = useState("5");
    const [quote, setQuote] = useState<any>(null);
    const [swapStatus, setSwapStatus] = useState("");

    if (status !== "logged-in" || !wallet) {
        return (
            <button onClick={() => login()}>
                Sign In
            </button>
        );
    }

    async function handleGetQuote() {
        if (!wallet) return;
        setSwapStatus("Fetching quote...");
        setQuote(null);

        const result = await getSwapQuote({
            walletAddress: wallet.address,
            fromChainId: FROM_CHAIN,
            toChainId: TO_CHAIN,
            fromToken: FROM_TOKEN,
            toToken: TO_TOKEN,
            fromAmount: parseUnits(
                amount,
                USDC_DECIMALS
            ).toString(),
        });

        if (result.error) {
            setSwapStatus(
                `Quote error: ${result.error}`
            );
            return;
        }
        setQuote(result);
        setSwapStatus("");
    }

    async function handleSwap() {
        if (!wallet || !quote) return;
        const evmWallet = EVMWallet.from(wallet);

        // Approve the LI.FI router to spend USDC
        if (quote.estimate.approvalAddress) {
            setSwapStatus("Approving USDC...");
            await evmWallet.sendTransaction({
                to: FROM_TOKEN,
                data: encodeFunctionData({
                    abi: erc20Abi,
                    functionName: "approve",
                    args: [
                        quote.estimate
                            .approvalAddress,
                        parseUnits(
                            amount,
                            USDC_DECIMALS
                        ),
                    ],
                }),
            });
        }

        // Execute the swap transaction
        setSwapStatus("Executing swap...");
        const tx = await evmWallet.sendTransaction({
            to: quote.transactionRequest.to,
            data: quote.transactionRequest.data,
            value: quote.transactionRequest.value
                ? BigInt(
                      quote.transactionRequest.value
                  )
                : 0n,
        });

        // Poll until bridge completes (5-minute timeout)
        const timeout = Date.now() + 5 * 60 * 1000;
        while (Date.now() < timeout) {
            await new Promise((r) =>
                setTimeout(r, 5_000)
            );
            const s = await getSwapStatus(
                tx.hash,
                FROM_CHAIN,
                TO_CHAIN
            );
            if (s.status === "DONE") {
                setSwapStatus("Bridge complete!");
                return;
            }
            if (s.status === "FAILED") {
                setSwapStatus("Bridge failed.");
                return;
            }
        }
        setSwapStatus("Bridge timed out.");
    }

    return (
        <div>
            <p>Wallet: {wallet.address}</p>
            <input
                type="number"
                value={amount}
                onChange={(e) =>
                    setAmount(e.target.value)
                }
            />
            <button onClick={handleGetQuote}>
                Get Quote
            </button>
            {quote && (
                <div>
                    <p>
                        {amount} USDC →{" "}
                        {formatUnits(
                            BigInt(
                                quote.estimate
                                    .toAmount
                            ),
                            USDC_DECIMALS
                        )}{" "}
                        USDC
                    </p>
                    <button onClick={handleSwap}>
                        Confirm Swap
                    </button>
                </div>
            )}
            {swapStatus && <p>{swapStatus}</p>}
        </div>
    );
}
The wallet must hold USDC on Base to perform the swap, and a small amount of ETH on Base to cover gas fees unless gas sponsorship is enabled. Cross-chain bridges operate on mainnet only.
5

Run the App

npm run dev
Visit website (e.g http://localhost:3000) to see your checkout!Sign in with your email, and you will see a wallet provisioned on Base. Enter a USDC amount, fetch a quote, and confirm the swap to bridge tokens to Polygon.

Swap Flow Summary

StepWhereWhat happens
Get QuoteServerCalls LI.FI /quote with token addresses and amount
ApproveClientSigns an ERC-20 approve transaction for the LI.FI router
Execute SwapClientSigns the swap transaction using calldata from the quote
Poll StatusServerCalls LI.FI /status every five seconds until DONE or FAILED

Customizing the Swap

You can bridge any token pair that LI.FI supports by changing the constants at the top of page.tsx:
const FROM_CHAIN = "1"; // Ethereum
const TO_CHAIN = "42161"; // Arbitrum
const FROM_TOKEN =
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC on Ethereum
const TO_TOKEN =
    "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // USDC on Arbitrum
See the full list of supported chains and tokens in the LI.FI API documentation.

Next Steps