Use this file to discover all available pages before exploring further.
This guide explains how to bridge and swap tokens across chains using the Relay API with Crossmint wallets. Relay is an intent-based cross-chain protocol that aggregates bridges and DEXs to execute transfers across 20+ EVM chains. Both human users and AI agents can use this integration to move assets between blockchains programmatically.
A production API Key with the scope: wallets:transactions.create (create in the Production Console)
This guide uses mainnet for all examples because testnet bridging support is limited. Start with small test amounts before moving larger sums.To follow along using Base as the origin chain, you also need:
ETH on Base mainnet for gas fees (not required if gas sponsorship is enabled)
For bridging: ETH on Base
For swapping: USDC on Base
A Relay API key is optional. Without one, requests are subject to default rate limits. To obtain a key, visit the Relay API Keys page.
Bridging moves the same token from one chain to another. This example bridges native ETH from Base to Arbitrum.
Both the Relay API and Crossmint wallets must support the source and destination chains. Most major EVM chains are supported, but for less common chains, verify support on the Relay supported chains page and the Crossmint supported chains list before proceeding.
High level steps:
Request a bridge quote from the Relay /quote/v2 endpoint
Execute the bridge transaction using the quote calldata
Poll the Relay /intents/status/v3 endpoint until the bridge completes
Cross-chain bridges primarily operate on mainnet. The wallet must hold sufficient tokens on the source chain and ETH for gas fees. Testnet support varies by route; see the Relay supported chains page for availability.
React
React Native
Node.js
REST
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-ui";import { parseEther } from "viem";const RELAY_API = "https://api.relay.link";const ORIGIN_CHAIN_ID = 8453; // Baseconst DESTINATION_CHAIN_ID = 42161; // Arbitrumconst ETH_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000";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 Relay const quoteRes = await fetch(`${RELAY_API}/quote/v2`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user: wallet.address, originChainId: ORIGIN_CHAIN_ID, destinationChainId: DESTINATION_CHAIN_ID, originCurrency: ETH_TOKEN_ADDRESS, destinationCurrency: ETH_TOKEN_ADDRESS, amount: parseEther(amount).toString(), tradeType: "EXACT_INPUT", }), }); if (!quoteRes.ok) { throw new Error(`Quote request failed: ${quoteRes.status}`); } const quote = await quoteRes.json(); // 2. Execute the bridge transaction // For native ETH bridges, Relay returns a single deposit step const txData = quote.steps[0].items[0].data; await evmWallet.sendTransaction({ to: txData.to, data: txData.data, value: txData.value ? BigInt(txData.value) : 0n, }); // 3. Poll Relay for bridge status const requestId = quote.steps[0].requestId; let status = "pending"; while (status === "pending") { await new Promise((r) => setTimeout(r, 5000)); const statusRes = await fetch( `${RELAY_API}/intents/status/v3?requestId=${requestId}` ); const s = await statusRes.json(); status = s.status; } if (status !== "success") { throw new Error(`Bridge failed with status: ${status}`); } } return ( <button onClick={() => bridge("0.001")}> Bridge ETH to Arbitrum </button> );}
import { CrossmintWallets, createCrossmint, EVMWallet } from "@crossmint/wallets-sdk";import { parseEther } from "viem";const crossmint = createCrossmint({ apiKey: "YOUR_SERVER_API_KEY",});const crossmintWallets = CrossmintWallets.from(crossmint);const wallet = await crossmintWallets.getWallet( "email:user@example.com", { chain: "base" });if (!wallet) throw new Error("Wallet not found");await wallet.useSigner({ type: "email", email: "user@example.com" });const evmWallet = EVMWallet.from(wallet);const RELAY_API = "https://api.relay.link";const ORIGIN_CHAIN_ID = 8453; // Baseconst DESTINATION_CHAIN_ID = 42161; // Arbitrumconst ETH_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000";const amount = "0.001";// 1. Get a bridge quote from Relayconst quoteRes = await fetch(`${RELAY_API}/quote/v2`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user: wallet.address, originChainId: ORIGIN_CHAIN_ID, destinationChainId: DESTINATION_CHAIN_ID, originCurrency: ETH_TOKEN_ADDRESS, destinationCurrency: ETH_TOKEN_ADDRESS, amount: parseEther(amount).toString(), tradeType: "EXACT_INPUT", }),});if (!quoteRes.ok) { throw new Error(`Quote request failed: ${quoteRes.status}`);}const quote = await quoteRes.json();// 2. Execute the bridge transaction// For native ETH bridges, Relay returns a single deposit stepconst txData = quote.steps[0].items[0].data;await evmWallet.sendTransaction({ to: txData.to, data: txData.data, value: txData.value ? BigInt(txData.value) : 0n,});// 3. Poll Relay for bridge statusconst requestId = quote.steps[0].requestId;let status = "pending";while (status === "pending") { await new Promise((r) => setTimeout(r, 5000)); const statusRes = await fetch( `${RELAY_API}/intents/status/v3?requestId=${requestId}` ); const s = await statusRes.json(); status = s.status;}if (status !== "success") { throw new Error(`Bridge failed with status: ${status}`);}console.log("Bridge complete!");
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.
1
Get a bridge quote from Relay
Call the Relay API/quote/v2 endpoint with the origin and destination chain, token addresses, and amount.
The response includes steps[] with transaction calldata and a requestId for tracking.
2
Execute the bridge transaction
Use the transaction data from quote.steps[0].items[0].data to send the bridge transaction via the Crossmint REST API.Follow the steps in the Send a transaction guide to submit the transaction via the Crossmint REST API.
3
Poll for bridge status
Call the Relay /intents/status/v3 endpoint every five seconds until the bridge completes.
const statusRes = await fetch( `https://api.relay.link/intents/status/v3?requestId=YOUR_REQUEST_ID`);const status = await statusRes.json();// status.status is "pending", "success", or "failure"
The bridge is complete when status is success. If failure, check the transaction on the block explorer for details.
Swapping exchanges one token for another across chains or within the same chain. This example swaps USDC on Base for USDC on Arbitrum. The same pattern works for any token pair that Relay supports.For ERC-20 swaps, Relay may return multiple steps — an approval step followed by a deposit step. The code below iterates through all steps to handle both cases.High level steps:
Request a swap quote from the Relay /quote/v2 endpoint
Execute all steps from the quote (approval + deposit)
Poll the Relay /intents/status/v3 endpoint until the swap completes
React
React Native
Node.js
REST
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-ui";import { parseUnits } from "viem";const RELAY_API = "https://api.relay.link";const ORIGIN_CHAIN_ID = 8453; // Baseconst DESTINATION_CHAIN_ID = 42161; // Arbitrumconst BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";const ARB_USDC = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";const USDC_DECIMALS = 6;export function SwapComponent() { const { wallet } = useWallet(); async function swap(amount: string) { if (!wallet) return; const evmWallet = EVMWallet.from(wallet); // 1. Get a swap quote from Relay const quoteRes = await fetch(`${RELAY_API}/quote/v2`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user: wallet.address, originChainId: ORIGIN_CHAIN_ID, destinationChainId: DESTINATION_CHAIN_ID, originCurrency: BASE_USDC, destinationCurrency: ARB_USDC, amount: parseUnits(amount, USDC_DECIMALS).toString(), tradeType: "EXACT_INPUT", }), }); if (!quoteRes.ok) { throw new Error(`Quote request failed: ${quoteRes.status}`); } const quote = await quoteRes.json(); // 2. Execute all steps (approval + deposit) for (const step of quote.steps) { for (const item of step.items) { if (item.status === "incomplete") { await evmWallet.sendTransaction({ to: item.data.to, data: item.data.data, value: item.data.value ? BigInt(item.data.value) : 0n, }); } } } // 3. Poll Relay for swap status const depositStep = quote.steps.find((s: { id: string }) => s.id === "deposit"); if (!depositStep) throw new Error("No deposit step found in quote response"); const requestId = depositStep.requestId; let status = "pending"; while (status === "pending") { await new Promise((r) => setTimeout(r, 5000)); const statusRes = await fetch( `${RELAY_API}/intents/status/v3?requestId=${requestId}` ); const s = await statusRes.json(); status = s.status; } if (status !== "success") { throw new Error(`Swap failed with status: ${status}`); } } return ( <button onClick={() => swap("5")}> Swap USDC to Arbitrum </button> );}
import { CrossmintWallets, createCrossmint, EVMWallet } from "@crossmint/wallets-sdk";import { 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", { chain: "base" });if (!wallet) throw new Error("Wallet not found");await wallet.useSigner({ type: "email", email: "user@example.com" });const evmWallet = EVMWallet.from(wallet);const RELAY_API = "https://api.relay.link";const ORIGIN_CHAIN_ID = 8453; // Baseconst DESTINATION_CHAIN_ID = 42161; // Arbitrumconst BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";const ARB_USDC = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";const USDC_DECIMALS = 6;const amount = "5";// 1. Get a swap quote from Relayconst quoteRes = await fetch(`${RELAY_API}/quote/v2`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user: wallet.address, originChainId: ORIGIN_CHAIN_ID, destinationChainId: DESTINATION_CHAIN_ID, originCurrency: BASE_USDC, destinationCurrency: ARB_USDC, amount: parseUnits(amount, USDC_DECIMALS).toString(), tradeType: "EXACT_INPUT", }),});if (!quoteRes.ok) { throw new Error(`Quote request failed: ${quoteRes.status}`);}const quote = await quoteRes.json();// 2. Execute all steps (approval + deposit)for (const step of quote.steps) { for (const item of step.items) { if (item.status === "incomplete") { await evmWallet.sendTransaction({ to: item.data.to, data: item.data.data, value: item.data.value ? BigInt(item.data.value) : 0n, }); } }}// 3. Poll Relay for swap statusconst depositStep = quote.steps.find((s: { id: string }) => s.id === "deposit");if (!depositStep) throw new Error("No deposit step found in quote response");const requestId = depositStep.requestId;let status = "pending";while (status === "pending") { await new Promise((r) => setTimeout(r, 5000)); const statusRes = await fetch( `${RELAY_API}/intents/status/v3?requestId=${requestId}` ); const s = await statusRes.json(); status = s.status;}if (status !== "success") { throw new Error(`Swap failed with status: ${status}`);}console.log("Swap complete!");
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.
1
Get a swap quote from Relay
Call the Relay API/quote/v2 endpoint with the origin and destination tokens and amount.
The response includes steps[] — for ERC-20 swaps this may include an approval step followed by a deposit step.
2
Execute all steps from the quote
Iterate through each step in quote.steps and each item in step.items. For items with status: "incomplete", send a transaction using the item.data fields (to, data, value).
Approval (if present) — grants the Relay contract permission to spend the source token
Deposit — executes the swap
Follow the steps in the Send a transaction guide to submit each transaction via the Crossmint REST API.
3
Poll for swap status
Call the Relay /intents/status/v3 endpoint every five seconds until the swap completes.
const statusRes = await fetch( `https://api.relay.link/intents/status/v3?requestId=YOUR_REQUEST_ID`);const status = await statusRes.json();// status.status is "pending", "success", or "failure"
The swap is complete when status is success. If failure, check the transaction on the block explorer for details.
Fund the wallet with a small amount of ETH on Base for gas (and USDC if testing the swap)
Run the bridge or swap code with a small amount (for example, 0.001 ETH for bridging or 1 USDC for swapping)
Verify the transaction completes by checking the status polling returns success
Confirm the destination wallet balance updated on the destination chain using the Check Balances guide
Relay also supports testnets (Base Sepolia, Sepolia, etc.), but testnet availability may vary. Check the Relay supported chains page for the latest testnet status.
Verify the wallet holds enough tokens on the source chain and ETH for gas fees.
Use the Check Balances guide to confirm token balances before bridging or swapping.
Quote Request Returns an Error
Ensure the user address is a valid wallet address and the amount is in the smallest unit (wei for ETH, or the token’s smallest denomination — for example, 6 decimals for USDC).
Verify the origin and destination chain IDs and token addresses are correct.
Check the Relay supported chains to confirm the route is available.
ERC-20 Swap Requires Multiple Transactions
For ERC-20 tokens, Relay may return an approval step before the deposit step. Make sure to iterate through all steps and all items within each step. Only execute items where status is "incomplete".
Bridge or Swap Status Stays Pending for a Long Time
Cross-chain operations can take several minutes depending on the route and chain congestion.
If the status does not change after 10 minutes, check the source transaction on a block explorer to verify it was included.
Deposits Not Detected by Relay (Smart Contract Wallets)
If Relay is not detecting deposits from a Crossmint smart wallet, try adding useReceiver: true to the quote request body. This routes the payment through a receiver contract that emits events, allowing the Relay solver to detect deposits from smart contract wallets.