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:# 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
This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:Then, update the tsconfig.json file: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.
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.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.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.
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: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:{
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: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:{
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: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:{
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.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;
}
When the spend completes, you should see output similar to: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. Use this flow to deposit and spend a Unified Balance with developer-controlled
wallets managed by Circle Wallets. The examples use Base Sepolia, Solana Devnet,
and Arc Testnet.Prerequisites
Before you begin, ensure that you’ve:
- Installed Node.js v22+.
- Set up Circle Wallets:
- Funded the wallets:
- Base Sepolia: testnet USDC from the
Circle Faucet and testnet ETH from a
public faucet.
- Solana Devnet: testnet USDC from the
Circle Faucet and SOL from the
Solana Faucet.
- Arc Testnet: fund the destination wallet if needed. USDC on Arc can cover
gas for the destination credit when you spend on Arc.
Step 1. Set up the project
1.1. Create the project and install dependencies
Create a new directory and install the App Kit SDK with the Circle Wallets
adapter and supporting tools:# Set up your directory and initialize a Node.js project
mkdir app-kit-unified-balance-circle-wallets
cd app-kit-unified-balance-circle-wallets
npm init -y
npm pkg set type=module
# Set up run scripts
npm pkg set scripts.deposit:base="tsx --env-file=.env deposit-base.ts"
npm pkg set scripts.deposit:solana="tsx --env-file=.env deposit-solana.ts"
npm pkg set scripts.balance="tsx --env-file=.env check-balance.ts"
npm pkg set scripts.spend="tsx --env-file=.env spend.ts"
# Install runtime dependencies
npm install @circle-fin/app-kit @circle-fin/adapter-circle-wallets tsx
# Install dev dependencies
npm install --save-dev typescript @types/node
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
This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:Then, update the tsconfig.json file:cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
1.3. Set environment variables
Create an .env file in the project directory:Add your credentials. Replace YOUR_API_KEY with your Circle Developer API key
and YOUR_ENTITY_SECRET with your entity secret:CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
Edit .env files in your IDE or editor so credentials are not leaked to your
shell history.
Step 2. Deposit into a Unified Balance
Circle Wallets uses developer-controlled addresses, so each operation includes
the wallet address explicitly.The API key, entity secret, and deposit wallet addresses must belong to the same
Circle Developer entity. Otherwise, Circle Wallets may fail to find the wallet
and return error 156001 during signing.
2.1. Deposit from Base Sepolia
Create a deposit-base.ts file. This script deposits 2.00 USDC from your Base
Sepolia Circle Wallets-controlled wallet into your Unified Balance.import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
const kit = new AppKit();
kit.on("*", (payload) => {
console.log("Event received:", payload);
});
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
const baseWalletAddress = "YOUR_BASE_SEPOLIA_WALLET_ADDRESS";
const result = await kit.unifiedBalance.deposit({
from: {
adapter,
chain: "Base_Sepolia",
address: baseWalletAddress,
},
amount: "2.00",
token: "USDC",
});
console.dir(result, { depth: null, colors: true });
Run the script:2.2. Deposit from Solana Devnet
Create a deposit-solana.ts file. This script deposits 1.00 USDC from your
Solana Devnet Circle Wallets-controlled wallet into your Unified Balance.import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
const kit = new AppKit();
kit.on("*", (payload) => {
console.log("Event received:", payload);
});
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
const solanaWalletAddress = "YOUR_SOLANA_DEVNET_WALLET_ADDRESS";
const result = await kit.unifiedBalance.deposit({
from: {
adapter,
chain: "Solana_Devnet",
address: solanaWalletAddress,
},
amount: "1.00",
token: "USDC",
});
console.dir(result, { depth: null, colors: true });
Run the script:2.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 3. Check your Unified Balance
After both deposits are finalized, create a check-balance.ts file. This script
queries the confirmed and pending Unified Balance for the Base Sepolia and
Solana Devnet source accounts.import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
const kit = new AppKit();
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
const baseWalletAddress = "YOUR_BASE_SEPOLIA_WALLET_ADDRESS";
const solanaWalletAddress = "YOUR_SOLANA_DEVNET_WALLET_ADDRESS";
const balances = await kit.unifiedBalance.getBalances({
sources: [
{
adapter,
address: baseWalletAddress,
chains: ["Base_Sepolia"],
},
{
adapter,
address: solanaWalletAddress,
chains: ["Solana_Devnet"],
},
],
networkType: "testnet",
includePending: true,
});
console.dir(balances, { depth: null, colors: true });
Run the script:Wait until totalConfirmedBalance is high enough for the spend you plan to
make.Step 4. Spend from the Unified Balance
Create a spend.ts file. This script spends 2.50 USDC on Arc Testnet. The
from sources do not include allocations, so the SDK chooses source
blockchains automatically from the available confirmed balances.import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
const kit = new AppKit();
kit.on("*", (payload) => {
console.log("Event received:", payload);
});
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
const baseWalletAddress = "YOUR_BASE_SEPOLIA_WALLET_ADDRESS";
const solanaWalletAddress = "YOUR_SOLANA_DEVNET_WALLET_ADDRESS";
const arcWalletAddress = "YOUR_ARC_TESTNET_WALLET_ADDRESS";
const result = await kit.unifiedBalance.spend({
amount: "2.50",
token: "USDC",
from: [
{
adapter,
address: baseWalletAddress,
sourceAccount: baseWalletAddress,
},
{
adapter,
address: solanaWalletAddress,
sourceAccount: solanaWalletAddress,
},
],
to: {
adapter,
chain: "Arc_Testnet",
address: arcWalletAddress,
recipientAddress: arcWalletAddress,
},
});
console.dir(result, { depth: null, colors: true });
Run the script:4.1. Verify the spend
Use the explorerUrl from the spend result to confirm that USDC arrived at the
recipient address on Arc Testnet. The spend result includes the allocations
selected by the SDK, the destination recipient, and the Arc Testnet mint
transaction hash.