Skip to main content
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.

Prerequisites

To use Relay 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)
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 Dashboard.

Bridge Tokens

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:
  1. Request a bridge quote from the Relay /quote/v2 endpoint
  2. Execute the bridge transaction using the quote calldata
  3. 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.
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; // Base
const DESTINATION_CHAIN_ID = 42161; // Arbitrum
const 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>
    );
}
See the React SDK reference for more details.

Swap Tokens

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:
  1. Request a swap quote from the Relay /quote/v2 endpoint
  2. Execute all steps from the quote (approval + deposit)
  3. Poll the Relay /intents/status/v3 endpoint until the swap completes
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; // Base
const DESTINATION_CHAIN_ID = 42161; // Arbitrum
const 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>
    );
}
See the React SDK reference for more details.

Customizing the Route

You can bridge or swap any token pair that Relay supports by changing the chain IDs and token addresses:
const ORIGIN_CHAIN_ID = 1; // Ethereum
const DESTINATION_CHAIN_ID = 10; // Optimism
const ORIGIN_CURRENCY =
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC on Ethereum
const DESTINATION_CURRENCY =
    "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"; // USDC on Optimism
See the full list of supported chains in the Relay documentation.

Testing the Integration

To test the integration end-to-end:
  1. Create a Crossmint wallet on Base using the Crossmint Console
  2. Fund the wallet with a small amount of ETH on Base for gas (and USDC if testing the swap)
  3. Run the bridge or swap code with a small amount (for example, 0.001 ETH for bridging or 1 USDC for swapping)
  4. Verify the transaction completes by checking the status polling returns success
  5. 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.

Troubleshooting

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.
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.
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".
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.
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.

Next Steps

Check Balances

Verify updated token balances after bridging or swapping on any chain

Bridge with LI.FI

Compare with the LI.FI bridging integration for alternative routes