Skip to main content
Bridge USDC between blockchains with the wallet model that matches your application. Use a browser wallet when the end user signs in the client, or use Circle Wallets when you manage developer-controlled wallets through Circle.
Use this flow to bridge USDC from a browser wallet. The wallet runs in the browser, and the user signs transactions in the wallet extension, so connect the wallet first and call kit.bridge() only after the wallet is available.This example uses the Viem adapter to bridge between two EVM-compatible blockchains in an existing browser app. The sample configuration uses Ethereum Sepolia and Arc Testnet, but you can use any supported EVM chains as the source or destination.

Prerequisites

Before you begin, ensure that you have:

Step 1. Set up the project

1.1. Create the project and install dependencies

Create a new directory, install the App Kit packages, and add local browser demo tooling:
Shell
# Set up your directory and initialize a Node.js project
mkdir app-kit-bridge-browser-wallet
cd app-kit-bridge-browser-wallet
npm init -y
npm pkg set type=module

# Install App Kit packages
npm install @circle-fin/app-kit @circle-fin/adapter-viem-v2 viem

# Install TypeScript and a local Vite dev server for the browser demo
npm install --save-dev typescript vite
Only need to bridge and want a lighter install than the full App Kit SDK? Install the standalone Bridge Kit instead: @circle-fin/bridge-kit

1.2. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
Shell
npx tsc --init
Then, update the tsconfig.json file:
Shell
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

Step 2. Connect a browser wallet

This step shows the core browser wallet integration flow: discover an EIP-6963 provider, create an App Kit adapter from the selected provider, and pass that adapter into an App Kit SDK method.The snippets below keep wallet discovery, wallet connection, and adapter setup in small helper functions for readability.

2.1. Discover a browser wallet with EIP-6963

This pattern is standards-based. The example uses MetaMask as the selected wallet, but the discovery flow works with any wallet that announces an EIP-6963 provider.
TypeScript
import type { EIP1193Provider } from "viem";

type EIP6963ProviderInfo = {
  uuid: string;
  name: string;
  icon: string;
  rdns: string;
};

type EIP6963ProviderDetail = {
  info: EIP6963ProviderInfo;
  provider: EIP1193Provider;
};

declare global {
  interface WindowEventMap {
    "eip6963:announceProvider": CustomEvent<EIP6963ProviderDetail>;
  }
}

async function discoverBrowserWallets(): Promise<EIP6963ProviderDetail[]> {
  const providers = new Map<string, EIP6963ProviderDetail>();

  const handleProviderAnnouncement = (
    event: WindowEventMap["eip6963:announceProvider"],
  ) => {
    providers.set(event.detail.info.uuid, event.detail);
  };

  window.addEventListener(
    "eip6963:announceProvider",
    handleProviderAnnouncement,
  );
  window.dispatchEvent(new Event("eip6963:requestProvider"));

  await new Promise((resolve) => window.setTimeout(resolve, 250));
  window.removeEventListener(
    "eip6963:announceProvider",
    handleProviderAnnouncement,
  );

  return [...providers.values()];
}

2.2. Connect the wallet and request account access

After you select a provider, request account access before attempting to bridge. This should happen in a user-triggered action such as a Connect wallet button.
TypeScript
async function connectWallet(provider: EIP1193Provider) {
  await provider.request({
    method: "eth_requestAccounts",
    params: undefined, // Required by the provider type even though this method has no params.
  });

  const accounts = (await provider.request({
    method: "eth_accounts",
    params: undefined, // Required by the provider type even though this method has no params.
  })) as string[];

  return {
    connectedAddress: accounts[0] ?? null,
  };
}
Keep wallet connection and bridging as separate user actions. This avoids overlapping wallet permission or chain-switch requests while a previous wallet prompt is still pending.

2.3. Create a Viem adapter from the selected wallet provider

Use the discovered provider to request account access, then create the App Kit adapter that signs bridge transactions in the browser:
TypeScript
import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";

async function connectBrowserWallet() {
  const providers = await discoverBrowserWallets();
  const selectedWallet =
    providers.find(
      ({ info }) => info.rdns === "io.metamask" || info.name === "MetaMask",
    ) ?? providers[0];

  if (!selectedWallet) {
    throw new Error("No EIP-6963 browser wallet found");
  }

  const { connectedAddress } = await connectWallet(selectedWallet.provider);

  const adapter = await createViemAdapterFromProvider({
    provider: selectedWallet.provider,
  });

  return {
    adapter,
    connectedAddress,
    walletName: selectedWallet.info.name,
  };
}
If multiple EVM wallets are installed, explicitly choose the wallet you want to use instead of relying on the first announced provider. The browser demo used to validate this quickstart prefers MetaMask when it is available.

Step 3. Bridge USDC

3.1. Pass the browser wallet adapter into kit.bridge()

This is the only App Kit-specific bridge call you need after the wallet is connected:
TypeScript
import { AppKit } from "@circle-fin/app-kit";

const kit = new AppKit();

async function bridgeUSDCWithBrowserWallet() {
  const { adapter, connectedAddress, walletName } =
    await connectBrowserWallet();

  let result = await kit.bridge({
    from: { adapter, chain: "Ethereum_Sepolia" },
    to: { adapter, chain: "Arc_Testnet" },
    amount: "1.00",
  });

  if (result.state === "error") {
    result = await kit.retryBridge(result, {
      from: adapter,
      to: adapter,
    });
  }

  console.log(`Submitted bridge with ${walletName}`, {
    connectedAddress,
    result,
  });
  return result;
}
Download the runnable browser demo to see the EVM-to-EVM bridge flow in action.

3.2. Observe bridge lifecycle events

To inspect the bridge flow while it runs, subscribe to Bridge Kit events before you call kit.bridge(). This is useful for seeing the runtime step order and payload shape. The companion browser demos render these events into an on-page <pre> element, but logging them to the console is enough for the core integration.
TypeScript
kit.on("*", (payload) => {
  console.log("Action:", payload);
});
Using other EVM chains? Change the chain values in kit.bridge() and ensure the connected wallet holds USDC on the source chain and enough gas to complete the transfer flow.
You can customize your bridges to collect a fee, use the Forwarding Service, or estimate gas and provider fees before bridging. Proceed only if the cost works for you.

3.3. Verify the transaction

After kit.bridge() resolves, inspect the returned steps array. Each transaction step includes an explorerUrl. Use those links to confirm the approve, burn, and mint steps for the amount you bridged.The following code is an example of how an approve event might look in the browser console after a successful bridge. The values are examples only and are not a real transaction:
Browser console
Event received: {
  protocol: "cctp",
  version: "v2",
  traceId: "550afd44ba4c6d1d1bf4880b9ded3840",
  values: {
    name: "approve",
    state: "success",
    txHash: "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
    data: {
      txHash:
        "0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
      status: "success",
      cumulativeGasUsed: 17138643n,
      gasUsed: 38617n,
      blockNumber: 8778959n,
      blockHash:
        "0xbeadfacefeed1234567890abcdef1234567890abcdef1234567890abcdef12",
      transactionIndex: 173,
      effectiveGasPrice: 1037232n,
    },
    explorerUrl:
      "https://testnet.arcscan.app/tx/0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcd",
  },
  method: "approve",
}

Extend: Add a Solana source wallet

If you want a Solana browser wallet as the source, keep the EVM destination adapter from the browser wallet flow above and add a Solana source adapter. The examples use Solana Devnet and Arc Testnet, but you can use Solana and any supported EVM chain as the destination.In this browser wallet flow, both wallets run in the browser and the user signs transactions in wallet extensions. Treat wallet connection and bridging as separate user actions: connect the destination EVM wallet first, connect the Solana source wallet second, then call kit.bridge() after both adapters are available.

Additional prerequisites

Before you begin, ensure that you have:
  • Worked through the EVM browser wallet flow above first. This Solana path adds a Solana source wallet to that same browser-wallet pattern.
  • Installed a Solana browser wallet that exposes window.solana.
  • Funded your Solana wallet with testnet USDC from the Circle Faucet.
  • Funded your Solana wallet with SOL for Solana Devnet transaction fees from the Solana Faucet.

Add the Solana dependencies

Add the Solana adapter dependency to the same project:
Shell
npm install @circle-fin/app-kit @circle-fin/adapter-solana @circle-fin/adapter-viem-v2 viem

Connect the Solana source wallet

This step extends the EVM browser wallet flow by adding a Solana source wallet. You will connect the Solana wallet, create a Solana source adapter, keep the EVM destination adapter, and pass both adapters into kit.bridge().The snippets below keep each part of the flow in small helper functions for readability. The companion browser demo wires this same sequence through handleEvmConnect(), handleSolanaConnect(), and handleBridge() in a runnable UI.

Connect the Solana wallet and request account access

This pattern assumes a Solana browser wallet that exposes window.solana. Keep wallet connection and bridging as separate user actions so the wallet is fully connected before you attempt the bridge. The destination EVM wallet should already be connected by the time you add the Solana source wallet.
TypeScript
import type { CreateSolanaAdapterFromProviderParams } from "@circle-fin/adapter-solana";

type SolanaWalletProvider = CreateSolanaAdapterFromProviderParams["provider"];

declare global {
  interface Window {
    solana?: SolanaWalletProvider;
  }
}

async function connectSolanaWallet(provider: SolanaWalletProvider) {
  const connection = await provider.connect();

  return {
    connectedAddress:
      connection.publicKey?.toString() ??
      provider.publicKey?.toString() ??
      null,
  };
}

Keep the EVM destination adapter and add a Solana source adapter

This Solana path builds on the EVM browser wallet flow above. Reuse the connected EVM wallet provider from that flow, then add a Solana provider and create one adapter for each chain:
TypeScript
import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
import { createSolanaAdapterFromProvider } from "@circle-fin/adapter-solana";
import type { EIP1193Provider } from "viem";

async function createBridgeAdapters(
  evmProvider: EIP1193Provider,
  solanaProvider: SolanaWalletProvider,
) {
  const evmAdapter = await createViemAdapterFromProvider({
    provider: evmProvider,
  });

  const solanaAdapter = await createSolanaAdapterFromProvider({
    provider: solanaProvider,
  });

  return {
    evmAdapter,
    solanaAdapter,
  };
}

Pass the browser wallet adapters into kit.bridge()

After you have a connected EVM provider from the earlier browser-wallet flow and a connected Solana provider from window.solana, create both adapters and pass them into kit.bridge():
TypeScript
import { AppKit } from "@circle-fin/app-kit";
import type { EIP1193Provider } from "viem";

const kit = new AppKit();

async function bridgeUSDCWithSolanaBrowserWallet(
  evmProvider: EIP1193Provider,
  solanaProvider: SolanaWalletProvider,
) {
  const { evmAdapter, solanaAdapter } = await createBridgeAdapters(
    evmProvider,
    solanaProvider,
  );

  const result = await kit.bridge({
    from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
    to: { adapter: evmAdapter, chain: "Arc_Testnet" },
    amount: "1.00",
  });

  console.log(
    "Submitted bridge from Solana browser wallet to EVM destination",
    {
      result,
    },
  );

  return result;
}

Retry a failed bridge attempt

If the first bridge attempt returns state: "error", retry it with the same freshly created adapters:
TypeScript
let result = await kit.bridge({
  from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
  to: { adapter: evmAdapter, chain: "Arc_Testnet" },
  amount: "1.00",
});

if (result.state === "error") {
  result = await kit.retryBridge(result, {
    from: solanaAdapter,
    to: evmAdapter,
  });
}
Download the runnable browser demo to see the Solana-to-EVM bridge flow in action.

Observe bridge lifecycle events

If you added the kit.on("*", (payload) => { ... }) listener in the previous step, it already captures Solana bridge events, and no additional subscription is needed.
TypeScript
kit.on("*", (payload) => {
  console.log("Action:", payload);
});
Using a different EVM chain as the destination? Change the to.chain value and ensure the connected Solana wallet holds USDC on the source chain and enough native gas to complete the transfer flow.
You can customize your bridges to collect a fee, use the Forwarding Service, or estimate gas and provider fees before bridging. Proceed only if the cost works for you.

Verify the transaction

After kit.bridge() resolves, inspect the returned steps array. Each transaction step includes an explorerUrl. Use those links to confirm the burn, attestation, and mint steps for the amount you bridged.The following code is an example of how a burn step might look in the browser console after a successful bridge. The values are examples only and are not a real transaction:
Browser console
steps: [
  {
    name: "burn",
    state: "success",
    txHash: "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm",
    data: {
      txHash:
        "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm",
      status: "success",
      blockNumber: 312456789n,
      blockHash: "HxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNK",
      transactionIndex: 0,
      gasUsed: 25000n,
      cumulativeGasUsed: 0n,
      effectiveGasPrice: 5000n,
      explorerUrl:
        "https://solscan.io/tx/5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKSNc7WLyFTmgL5RFGujWNqEbUBdNKRkHmx7ZRQR3FVhdEwxKHm?cluster=devnet",
    },
  },
];