Prerequisites
To use LI.FI bridging with Crossmint wallets, you need:- A Crossmint wallet on any supported EVM chain
- A production API Key with the scope:
wallets:transactions.create(create in the Production Console)
- USDC on Base mainnet in the wallet
- ETH on Base for gas fees (not required if gas sponsorship is enabled)
Bridge Tokens
High level steps to bridging tokens with LI.FI and Crossmint:- Request a bridge quote from the LI.FI API
- Approve the LI.FI router to spend the source token
- Execute the bridge transaction using the quote calldata
- Poll the LI.FI status endpoint until the bridge completes
Cross-chain bridges operate on mainnet only. The wallet must hold sufficient tokens on the source chain and ETH for gas fees.
- React
- Node.js
- React Native
- Swift
- Kotlin
- REST
Report incorrect code
Copy
Ask AI
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-ui";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
const LIFI_API = "https://li.quest/v1";
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 function BridgeComponent() {
const { wallet } = useWallet();
async function bridge(amount: string) {
if(!wallet) return;
const evmWallet = EVMWallet.from(wallet);
// 1. Get a bridge quote from the LI.FI API
const quoteRes = await fetch(
`${LIFI_API}/quote?` +
new URLSearchParams({
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
fromToken: FROM_TOKEN,
toToken: TO_TOKEN,
fromAmount: parseUnits(amount, USDC_DECIMALS).toString(),
fromAddress: wallet.address,
integrator: "crossmint",
})
);
if (!quoteRes.ok) {
throw new Error(`Quote request failed: ${quoteRes.status}`);
}
const quote = await quoteRes.json();
// 2. Approve the LI.FI router to spend USDC
if (quote.estimate.approvalAddress) {
await evmWallet.sendTransaction({
to: FROM_TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
quote.estimate.approvalAddress,
parseUnits(amount, USDC_DECIMALS),
],
}),
});
}
// 3. Execute the bridge transaction
const tx = await evmWallet.sendTransaction({
to: quote.transactionRequest.to,
data: quote.transactionRequest.data,
value: quote.transactionRequest.value
? BigInt(quote.transactionRequest.value)
: 0n,
});
// 4. Poll the LI.FI status endpoint until completion
let status = "PENDING";
while (status === "PENDING" || status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`${LIFI_API}/status?` +
new URLSearchParams({
txHash: tx.hash,
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
})
);
const s = await statusRes.json();
status = s.status;
}
}
return (
<button onClick={() => bridge("5")}>Bridge Tokens</button>
);
}
Report incorrect code
Copy
Ask AI
import { CrossmintWallets, createCrossmint, EVMWallet } from "@crossmint/wallets-sdk";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
const crossmint = createCrossmint({
apiKey: <YOUR_SERVER_API_KEY>,
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const wallet = await crossmintWallets.getWallet(
"email:user@example.com:evm",
{ chain: "base", signer: { type: "email" } }
);
if(!wallet) throw new Error("Wallet not found");
const evmWallet = EVMWallet.from(wallet);
const LIFI_API = "https://li.quest/v1";
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;
const amount = "5";
// 1. Get a bridge quote from the LI.FI API
const quoteRes = await fetch(
`${LIFI_API}/quote?` +
new URLSearchParams({
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
fromToken: FROM_TOKEN,
toToken: TO_TOKEN,
fromAmount: parseUnits(amount, USDC_DECIMALS).toString(),
fromAddress: wallet.address,
integrator: "crossmint",
})
);
if (!quoteRes.ok) {
throw new Error(`Quote request failed: ${quoteRes.status}`);
}
const quote = await quoteRes.json();
// 2. Approve the LI.FI router to spend USDC
if (quote.estimate.approvalAddress) {
await evmWallet.sendTransaction({
to: FROM_TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
quote.estimate.approvalAddress,
parseUnits(amount, USDC_DECIMALS),
],
}),
});
}
// 3. Execute the bridge transaction
const tx = await evmWallet.sendTransaction({
to: quote.transactionRequest.to,
data: quote.transactionRequest.data,
value: quote.transactionRequest.value
? BigInt(quote.transactionRequest.value)
: 0n,
});
// 4. Poll the LI.FI status endpoint until completion
let status = "PENDING";
while (status === "PENDING" || status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`${LIFI_API}/status?` +
new URLSearchParams({
txHash: tx.hash,
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
})
);
const s = await statusRes.json();
status = s.status;
}
console.log("Bridge complete!");
Report incorrect code
Copy
Ask AI
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-native-ui";
import { View, Button } from "react-native";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
const LIFI_API = "https://li.quest/v1";
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 function BridgeComponent() {
const { wallet } = useWallet();
async function bridge(amount: string) {
if(!wallet) return;
const evmWallet = EVMWallet.from(wallet);
// 1. Get a bridge quote from the LI.FI API
const quoteRes = await fetch(
`${LIFI_API}/quote?` +
new URLSearchParams({
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
fromToken: FROM_TOKEN,
toToken: TO_TOKEN,
fromAmount: parseUnits(amount, USDC_DECIMALS).toString(),
fromAddress: wallet.address,
integrator: "crossmint",
})
);
if (!quoteRes.ok) {
throw new Error(`Quote request failed: ${quoteRes.status}`);
}
const quote = await quoteRes.json();
// 2. Approve the LI.FI router to spend USDC
if (quote.estimate.approvalAddress) {
await evmWallet.sendTransaction({
to: FROM_TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
quote.estimate.approvalAddress,
parseUnits(amount, USDC_DECIMALS),
],
}),
});
}
// 3. Execute the bridge transaction
const tx = await evmWallet.sendTransaction({
to: quote.transactionRequest.to,
data: quote.transactionRequest.data,
value: quote.transactionRequest.value
? BigInt(quote.transactionRequest.value)
: 0n,
});
// 4. Poll the LI.FI status endpoint until completion
let status = "PENDING";
while (status === "PENDING" || status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`${LIFI_API}/status?` +
new URLSearchParams({
txHash: tx.hash,
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
})
);
const s = await statusRes.json();
status = s.status;
}
}
return (
<View>
<Button title="Bridge Tokens" onPress={() => bridge("5")} />
</View>
);
}
Report incorrect code
Copy
Ask AI
import CrossmintClient
import Wallet
import Foundation
let sdk = CrossmintSDK.shared
let wallet = try await sdk.crossmintWallets
.getOrCreateWallet(
chain: .base,
signer: .email(email: "user@example.com")
)
let evmWallet = try EVMWallet.from(wallet: wallet)
let fromChain = "8453"
let toChain = "137"
let fromToken = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
let toToken = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"
let amount = "5000000" // 5 USDC (6 decimals)
// 1. Get a bridge quote from the LI.FI API
var quoteURL = URLComponents(string: "https://li.quest/v1/quote")!
quoteURL.queryItems = [
URLQueryItem(name: "fromChain", value: fromChain),
URLQueryItem(name: "toChain", value: toChain),
URLQueryItem(name: "fromToken", value: fromToken),
URLQueryItem(name: "toToken", value: toToken),
URLQueryItem(name: "fromAmount", value: amount),
URLQueryItem(name: "fromAddress", value: wallet.address),
URLQueryItem(name: "integrator", value: "crossmint"),
]
let (quoteData, quoteResponse) = try await URLSession.shared.data(from: quoteURL.url!)
guard let httpResponse = quoteResponse as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let quote = try JSONSerialization.jsonObject(with: quoteData) as! [String: Any]
let estimate = quote["estimate"] as! [String: Any]
let txRequest = quote["transactionRequest"] as! [String: Any]
// 2. Approve the LI.FI router to spend USDC
// Encodes ERC-20 approve(address,uint256) — selector 0x095ea7b3
// Note: UInt64 supports up to ~18.4e18. For tokens with 18 decimals
// and large amounts, use a big-integer hex conversion instead.
func leftPad(_ str: String, toLength len: Int) -> String {
String(repeating: "0", count: max(0, len - str.count)) + str
}
if let approvalAddress = estimate["approvalAddress"] as? String {
let addr = leftPad(String(approvalAddress.dropFirst(2)), toLength: 64)
let amt = leftPad(String(UInt64(amount)!, radix: 16), toLength: 64)
let approveData = "0x095ea7b3" + addr + amt
let _ = try await evmWallet.sendTransaction(
to: fromToken,
value: "0",
data: approveData
)
}
// 3. Execute the bridge transaction
let result = try await evmWallet.sendTransaction(
to: txRequest["to"] as! String,
value: (txRequest["value"] as? String) ?? "0",
data: txRequest["data"] as! String
)
// 4. Poll the LI.FI status endpoint until completion
var bridgeStatus = "PENDING"
while bridgeStatus == "PENDING" || bridgeStatus == "NOT_FOUND" {
try await Task.sleep(nanoseconds: 5_000_000_000)
var statusURL = URLComponents(string: "https://li.quest/v1/status")!
statusURL.queryItems = [
URLQueryItem(name: "txHash", value: result.hash),
URLQueryItem(name: "fromChain", value: fromChain),
URLQueryItem(name: "toChain", value: toChain),
]
let (statusData, _) = try await URLSession.shared.data(from: statusURL.url!)
let statusJSON = try JSONSerialization.jsonObject(with: statusData) as! [String: Any]
bridgeStatus = statusJSON["status"] as! String
}
Report incorrect code
Copy
Ask AI
import com.crossmint.kotlin.compose.LocalCrossmintSDK
import com.crossmint.kotlin.types.EVMChain
import com.crossmint.kotlin.types.SignerData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import java.net.HttpURLConnection
import java.net.URL
val API_KEY = "<YOUR_API_KEY>"
val SIGNER_EMAIL = "user@example.com"
val LIFI_API = "https://li.quest/v1"
val CROSSMINT_API = "https://www.crossmint.com/api/2025-06-09"
val FROM_CHAIN = "8453" // Base
val TO_CHAIN = "137" // Polygon
val FROM_TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" // USDC on Base
val TO_TOKEN = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" // USDC on Polygon
val AMOUNT = "5000000" // 5 USDC (6 decimals)
// Access the SDK — must be called inside a CrossmintSDKProvider composable
val sdk = LocalCrossmintSDK.current
val wallet = sdk.crossmintWallets
.getOrCreateWallet(EVMChain.Base, SignerData.Email(SIGNER_EMAIL))
.getOrThrow()
val json = Json { ignoreUnknownKeys = true }
// 1. Get a bridge quote from the LI.FI API
val quoteText = withContext(Dispatchers.IO) {
val conn = URL(
"$LIFI_API/quote?fromChain=$FROM_CHAIN&toChain=$TO_CHAIN" +
"&fromToken=$FROM_TOKEN&toToken=$TO_TOKEN" +
"&fromAmount=$AMOUNT&fromAddress=${wallet.address}&integrator=crossmint"
).openConnection() as HttpURLConnection
check(conn.responseCode == 200) { "Quote request failed: ${conn.responseCode}" }
conn.inputStream.bufferedReader().readText()
}
val quote = json.parseToJsonElement(quoteText).jsonObject
val estimate = quote["estimate"]!!.jsonObject
val txRequest = quote["transactionRequest"]!!.jsonObject
// The Kotlin SDK's send() only supports simple token transfers, not raw calldata.
// The REST API is used here to create approval and bridge transactions directly.
suspend fun sendEvmTransaction(to: String, data: String, value: String = "0"): String {
val body = buildJsonObject {
putJsonObject("params") {
put("chain", "base")
put("signer", "email:$SIGNER_EMAIL")
putJsonArray("calls") {
add(buildJsonObject {
put("to", to)
put("value", value)
put("data", data)
})
}
}
}.toString()
val responseText = withContext(Dispatchers.IO) {
val conn = URL("$CROSSMINT_API/wallets/${wallet.address}/transactions")
.openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.setRequestProperty("X-API-KEY", API_KEY)
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
conn.outputStream.write(body.toByteArray())
val code = conn.responseCode
if (code !in 200..299) {
val err = conn.errorStream?.bufferedReader()?.readText()
error("Transaction request failed ($code): $err")
}
conn.inputStream.bufferedReader().readText()
}
val transactionId = json.parseToJsonElement(responseText)
.jsonObject["id"]!!.jsonPrimitive.content
val tx = wallet.approve(transactionId).getOrThrow()
return tx.onChain.txId ?: tx.onChain.userOperationHash
?: error("Transaction completed but no hash was returned")
}
// 2. Approve the LI.FI router to spend USDC
// Encodes ERC-20 approve(address,uint256) — selector 0x095ea7b3
// Note: Long supports up to ~9.2e18. For tokens with 18 decimals
// and large amounts, use a BigInteger hex conversion instead.
val approvalAddress = estimate["approvalAddress"]?.jsonPrimitive?.contentOrNull
if (approvalAddress != null) {
val addr = approvalAddress.removePrefix("0x").padStart(64, '0')
val amt = AMOUNT.toLong().toString(16).padStart(64, '0')
sendEvmTransaction(to = FROM_TOKEN, data = "0x095ea7b3$addr$amt")
}
// 3. Execute the bridge transaction
val txHash = sendEvmTransaction(
to = txRequest["to"]!!.jsonPrimitive.content,
data = txRequest["data"]!!.jsonPrimitive.content,
value = txRequest["value"]?.jsonPrimitive?.contentOrNull ?: "0",
)
// 4. Poll the LI.FI status endpoint until completion
var bridgeStatus = "PENDING"
while (bridgeStatus == "PENDING" || bridgeStatus == "NOT_FOUND") {
delay(5_000)
val statusText = withContext(Dispatchers.IO) {
val conn = URL("$LIFI_API/status?txHash=$txHash&fromChain=$FROM_CHAIN&toChain=$TO_CHAIN")
.openConnection() as HttpURLConnection
check(conn.responseCode == 200) { "Status request failed: ${conn.responseCode}" }
conn.inputStream.bufferedReader().readText()
}
bridgeStatus = json.parseToJsonElement(statusText).jsonObject["status"]!!.jsonPrimitive.content
}
Transactions must be approved by one of the wallet’s signers.
The SDK handles this automatically, but with the REST API you must approve the transaction to complete it.
Get a bridge quote from the LI.FI API
Call the LI.FI API The response includes
/quote endpoint with the source and destination chain, token addresses, and amount.Report incorrect code
Copy
Ask AI
curl --request GET \
--url 'https://li.quest/v1/quote?fromChain=8453&toChain=137&fromToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&toToken=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359&fromAmount=5000000&fromAddress=<wallet-address>&integrator=crossmint' \
--header 'Content-Type: application/json'
estimate.approvalAddress and transactionRequest with the bridge calldata.Send the approve and bridge transactions
Use the quote response to send two transactions via the Crossmint REST API:
- Approve — call the USDC contract with ERC-20
approvecalldata targetingestimate.approvalAddress - Bridge — send
transactionRequest.to,transactionRequest.data, andtransactionRequest.value
Poll for bridge status
Call the LI.FI The bridge is complete when
/status endpoint every five seconds until the bridge completes.Report incorrect code
Copy
Ask AI
curl --request GET \
--url 'https://li.quest/v1/status?txHash=<bridge-tx-hash>&fromChain=8453&toChain=137' \
--header 'Content-Type: application/json'
status is DONE. If FAILED, check the transaction on the block explorer for details.Customizing the Bridge
You can bridge any token pair that LI.FI supports by changing the chain and token constants. You can also perform cross-chain swaps — changing both the asset and the chain in a single operation:Report incorrect code
Copy
Ask AI
const FROM_CHAIN = "1"; // Ethereum
const TO_CHAIN = "42161"; // Arbitrum
const FROM_TOKEN =
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC on Ethereum
const TO_TOKEN =
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // USDC on Arbitrum
Troubleshooting
Transaction fails with insufficient balance
Transaction fails with insufficient balance
Verify the wallet holds enough USDC on Base and ETH for gas fees.
Use the Check Balances guide to confirm token balances before bridging.
ERC-20 approval transaction is rejected
ERC-20 approval transaction is rejected
Ensure the approval amount is greater than or equal to the bridge amount.
If a previous approval exists for a smaller amount, send a new approval transaction with the correct value.
Verify the API key has the
wallets:transactions.create scope.Bridge status stays PENDING for a long time
Bridge status stays PENDING for a long time
Cross-chain bridges can take several minutes depending on the route and chain congestion.
The LI.FI
/status endpoint returns PENDING until both the source and destination chain transactions confirm.
If the status does not change after 10 minutes, check the source transaction on a block explorer to verify it was included.
