Skip to main content
Use this quickstart to deposit USDC into a Unified Balance, check the combined balance, and spend from it on another blockchain. The examples deposit from Base Sepolia and Solana Devnet, then spend on Arc Testnet. Choose 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 deposit and spend a Unified Balance with connected browser wallets. The examples use Base Sepolia, Solana Devnet, and Arc Testnet.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+.
  • Created an EVM wallet using a wallet provider such as MetaMask and added the Base Sepolia and Arc Testnet networks.
  • Created a Solana wallet (for example, Phantom or Solflare) on Devnet.
  • Funded your wallets with testnet tokens:
    • Get testnet USDC from the Circle Faucet on Base Sepolia and Solana Devnet.
    • Get testnet ETH on Base Sepolia from a public faucet (needed for deposit and spend transactions on Base Sepolia).
    • Get SOL for Solana Devnet from the Solana Faucet.
    • Fund the connected EVM wallet on Arc Testnet if needed (USDC on Arc can cover gas for the destination credit when you spend on Arc).
  • Obtained an Arc Testnet address that will receive USDC when you spend on Arc Testnet.

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-unified-balance-browser-wallet
cd app-kit-unified-balance-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 @circle-fin/adapter-solana

# Install TypeScript and a local Vite dev server for the browser demo
npm install --save-dev typescript vite
Only need a Unified Balance and want a lighter install than the full App Kit SDK? Install the standalone Unified Balance Kit instead: @circle-fin/unified-balance-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 browser wallets

This step shows the core browser wallet integration flow: discover an EIP-6963 provider, create App Kit adapters from the selected providers, and pass those adapters into App Kit SDK methods.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 calling an App Kit SDK method. 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 App Kit actions as separate user actions. This avoids overlapping wallet permission or chain-switch requests while a previous wallet prompt is still pending.

2.3. Create an EVM adapter for Unified Balance

This quickstart uses adapter-only balance checks and spend sources so the SDK can choose source blockchains automatically. Configure the EVM adapter with the EVM chains used in this flow so automatic allocation has chain context for balance discovery.
TypeScript
import { ArcTestnet, BaseSepolia } from "@circle-fin/app-kit/chains";
import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";

async function connectEvmBrowserWallet() {
  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,
    capabilities: {
      supportedChains: [BaseSepolia, ArcTestnet],
    },
  });

  return {
    adapter,
    connectedAddress,
    walletName: selectedWallet.info.name,
  };
}

2.4. Connect the Solana wallet and create a Solana adapter

This pattern assumes a Solana browser wallet that exposes window.solana. Keep wallet connection and App Kit actions as separate user actions so the wallet is fully connected before you call an App Kit SDK method.
TypeScript
import { SolanaDevnet } from "@circle-fin/app-kit/chains";
import { createSolanaAdapterFromProvider } from "@circle-fin/adapter-solana";
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,
  };
}

async function connectSolanaBrowserWallet() {
  const provider = window.solana;

  if (!provider) {
    throw new Error("No Solana browser wallet found");
  }

  const { connectedAddress } = await connectSolanaWallet(provider);

  const adapter = await createSolanaAdapterFromProvider({
    provider,
    capabilities: {
      supportedChains: [SolanaDevnet],
    },
  });

  return {
    adapter,
    connectedAddress,
  };
}

Step 3. Deposit into a Unified Balance

In this step, you’ll deposit from Base Sepolia and Solana Devnet. The EVM deposit uses the EVM browser wallet adapter from the previous step. The Solana deposit uses the Solana browser wallet adapter from the previous step.The examples in the remaining steps reuse the same kit instance and Adapter type.

3.1. Deposit from Base Sepolia

Call kit.unifiedBalance.deposit with the connected EVM browser wallet adapter. Before the deposit, switch the browser wallet to Base Sepolia so the deposit authorization is signed on the source chain:
TypeScript
import { AppKit, type Adapter } from "@circle-fin/app-kit";
import { resolveChainIdentifier } from "@circle-fin/adapter-viem-v2";

const kit = new AppKit();

async function depositFromBaseSepolia(evmAdapter: Adapter) {
  const chain = resolveChainIdentifier("Base_Sepolia");

  if (chain.type !== "evm") {
    throw new Error(`${chain.name} is not an EVM chain`);
  }

  await evmAdapter.ensureChain(chain);

  const result = await kit.unifiedBalance.deposit({
    from: { adapter: evmAdapter, chain: "Base_Sepolia" },
    amount: "2.00",
    token: "USDC",
  });

  console.dir(result, { depth: null });

  return result;
}
You’ll see output like:
Shell
{
  amount: "2.00",
  token: "USDC",
  chain: "Base_Sepolia",
  txHash: "0x...",
  explorerUrl: "https://sepolia.basescan.org/tx/0x...",
  ...
}

3.2. Deposit from Solana Devnet

Call kit.unifiedBalance.deposit with the Solana browser wallet adapter:
TypeScript
async function depositFromSolanaDevnet(solanaAdapter: Adapter) {
  const result = await kit.unifiedBalance.deposit({
    from: { adapter: solanaAdapter, chain: "Solana_Devnet" },
    amount: "1.00",
    token: "USDC",
  });

  console.dir(result, { depth: null });

  return result;
}
You’ll see output like:
Shell
{
  amount: "1.00",
  token: "USDC",
  chain: "Solana_Devnet",
  txHash: "2k41...",
  explorerUrl: "https://solscan.io/tx/2k41...?cluster=devnet",
  ...
}

3.3. Verify the deposits

Open the explorerUrl from each deposit result and confirm the onchain transactions on Base Sepolia and Solana Devnet. When both deposits are finalized, continue to the next step.

Step 4. Check your Unified Balance

In this step, you query your Unified Balance across the Base Sepolia and Solana Devnet depositors and print the confirmed and pending amounts.

4.1. Check balances

Call kit.unifiedBalance.getBalances with the same browser wallet adapters you used for the deposits:
TypeScript
async function checkUnifiedBalance(
  evmAdapter: Adapter,
  solanaAdapter: Adapter,
) {
  const balances = await kit.unifiedBalance.getBalances({
    // Both wallets that deposited, one adapter per source.
    sources: [{ adapter: evmAdapter }, { adapter: solanaAdapter }],
    networkType: "testnet",
    includePending: true,
  });

  console.dir(balances, { depth: null });

  return balances;
}
You’ll see output like:
Shell
{
  token: "USDC",
  totalConfirmedBalance: "3.00",
  totalPendingBalance: "0.00",
  breakdown: [
    {
      depositor: "0x...",
      totalConfirmed: "2.00",
      totalPending: "0.00",
      breakdown: [{ chain: "Base_Sepolia", confirmedBalance: "2.00", ... }]
    },
    {
      depositor: "...",
      totalConfirmed: "1.00",
      totalPending: "0.00",
      breakdown: [{ chain: "Solana_Devnet", confirmedBalance: "1.00", ... }]
    }
  ]
}
After a deposit, funds can appear in totalPendingBalance before they are reflected in totalConfirmedBalance. Wait until the confirmed balance is sufficient before you spend.

Step 5. Spend from the combined balance

In this step, you spend USDC on Arc Testnet from your Unified Balance.

5.1. Spend on Arc Testnet

Collect the recipient address from your app UI, then pass it with the connected wallet adapters into the spend function. This code spends 2.50 USDC on Arc Testnet for the recipient. The App Kit SDK chooses how much USDC to use from each blockchain.
TypeScript
async function spendFromUnifiedBalance(
  evmAdapter: Adapter,
  solanaAdapter: Adapter,
  recipientAddress: string,
) {
  console.log(`Spending 2.50 USDC on Arc_Testnet for ${recipientAddress}...`);

  const result = await kit.unifiedBalance.spend({
    amount: "2.50",
    token: "USDC",
    from: [{ adapter: evmAdapter }, { adapter: solanaAdapter }],
    to: {
      adapter: evmAdapter,
      chain: "Arc_Testnet",
      recipientAddress,
    },
  });

  console.dir(result, { depth: null });

  return result;
}
You can customize your Unified Balance to collect a custom fee from end users, estimate fees before spending, select source blockchains and allocations to fund a balance, or use the Forwarding Service.
When the spend completes, you should see output similar to:
Shell
Spending 2.50 USDC on Arc_Testnet for 0x...

{ recipientAddress: "0x...", destinationChain: "Arc Testnet", txHash: "0x...", ... }

5.2. Verify the spend

Use the explorerUrl from the spend result to confirm that USDC arrived at the recipient address on Arc Testnet. The received amount can be less than the requested spend after fees. For more on fees, see How Unified Balance fees work.