Skip to main content
Delegation lets one address authorize another address to sign spends from its Unified Balance. The owner account keeps custody of the funds, while the delegate signs spend intents for authorized source blockchains. Delegated spends are typically a server-side pattern: the owner authorizes a delegate once, then a backend service signs future spend intents without asking the owner wallet to sign each spend. The mechanism is wallet-agnostic; any supported owner wallet can authorize a compatible EOA delegate. In this quickstart, you’ll use Circle Wallets for both wallets: an owner wallet that holds the Unified Balance and an EOA delegate wallet that signs spends. You’ll use the delegate wallet to deposit into the owner’s Unified Balance, authorize the delegate on Base Sepolia, check the owner’s Unified Balance, and spend on Arc Testnet with the Forwarding Service.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+.
  • Set up Circle Wallets:
    • Obtained a API Key and entity secret from the Circle Console.
    • Created an owner wallet and an EOA delegate wallet on Base Sepolia. The owner wallet holds the Unified Balance, and the delegate wallet signs delegated spends after authorization.
  • Funded the Base Sepolia wallets:
  • Obtained an Arc Testnet recipient address that will receive the USDC.

Step 1. Set up your 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:
Shell
# Set up your directory and initialize a Node.js project
mkdir unified-balance-delegate
cd unified-balance-delegate
npm init -y
npm pkg set type=module

# Set up run scripts
npm pkg set scripts.deposit="tsx --env-file=.env delegate-deposit.ts"
npm pkg set scripts.authorize="tsx --env-file=.env delegate-authorize.ts"
npm pkg set scripts.balance="tsx --env-file=.env delegate-check-balance.ts"
npm pkg set scripts.spend="tsx --env-file=.env delegate-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

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

1.3. Set environment variables

Create an .env file in the project directory:
Shell
touch .env
Add your credentials. Replace YOUR_API_KEY with your Circle Developer API key and YOUR_ENTITY_SECRET with your entity secret:
.env
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 the owner’s Unified Balance

In this step, the delegate wallet deposits USDC into the owner’s Unified Balance from Base Sepolia.

2.1. Create the deposit script

Create a delegate-deposit.ts file. In this script, the delegate wallet deposits 2.00 USDC from Base Sepolia into the owner’s Unified Balance.
delegate-deposit.ts
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

const DEPOSIT_AMOUNT = "2.00";

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 ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";
const delegateWalletAddress = "YOUR_BASE_SEPOLIA_DELEGATE_WALLET_ADDRESS";

const result = await kit.unifiedBalance.depositFor({
  from: {
    adapter,
    chain: "Base_Sepolia",
    address: delegateWalletAddress,
  },
  amount: DEPOSIT_AMOUNT,
  token: "USDC",
  depositAccount: ownerWalletAddress,
});

console.dir(result, { depth: null, colors: true });
depositFor is permissionless. Any wallet can fund another account’s Unified Balance. This quickstart uses it so the delegate funds the owner before the owner grants spend authorization.

2.2. Run the deposit script

In your terminal, run:
Shell
npm run deposit
You’ll see output like:
Shell
{
  amount: '2.00',
  token: 'USDC',
  depositedTo: '0x...',
  depositedBy: '0x...',
  chain: 'Base_Sepolia',
  txHash: '0x...',
  explorerUrl: 'https://sepolia.basescan.org/tx/0x...'
}

2.3. Verify the deposit

Open the explorerUrl from the deposit result to confirm the onchain transaction on Base Sepolia.

Step 3. Authorize the delegate

In this step, the owner wallet grants the delegate permission to spend from its Unified Balance on a specific blockchain.

3.1. Create the authorize script

Create a delegate-authorize.ts file. In this script, the owner wallet authorizes the delegate to spend from its Unified Balance on Base Sepolia:
delegate-authorize.ts
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 ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";
const delegateWalletAddress = "YOUR_BASE_SEPOLIA_DELEGATE_WALLET_ADDRESS";

async function main() {
  const status = await kit.unifiedBalance.getDelegateStatus({
    from: {
      adapter,
      chain: "Base_Sepolia",
      address: ownerWalletAddress,
    },
    delegateAddress: delegateWalletAddress,
  });

  if (status === "ready") {
    console.log(
      `Delegate ${delegateWalletAddress} is already authorized on Base_Sepolia.`,
    );
    return;
  }

  if (status === "pending") {
    console.log(
      `Delegate ${delegateWalletAddress} is still pending on Base_Sepolia. Wait and run this script again.`,
    );
    return;
  }

  // addDelegate: owner-signed transaction granting the delegate spend rights.
  const result = await kit.unifiedBalance.addDelegate({
    from: {
      adapter,
      chain: "Base_Sepolia",
      address: ownerWalletAddress,
    },
    delegateAddress: delegateWalletAddress,
  });

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

void main();
addDelegate is an onchain transaction signed by the owner wallet. Once authorized, the delegate can spend repeatedly on the same blockchain without reauthorization. Authorize only delegate addresses you control, and revoke access when it is no longer needed. Authorization is source-blockchain specific. See Manage Delegates for details.

3.2. Run the authorize script

In your terminal, run:
Shell
npm run authorize
You’ll see output like:
Shell
{
  account: '0x...',
  delegateAddress: '0x...',
  chain: 'Base_Sepolia',
  state: 'added',
  txHash: '0x...',
  explorerUrl: 'https://sepolia.basescan.org/tx/0x...'
}
If status is already 'ready', the script exits without calling addDelegate. If status is 'pending', it asks you to wait and run the script again. Otherwise it submits addDelegate.

Step 4. Check the owner’s Unified Balance

In this step, you check the owner’s Unified Balance by address.

4.1. Create the balance check script

Create a delegate-check-balance.ts file. This script prints the owner’s confirmed and pending Unified Balance totals:
delegate-check-balance.ts
import { AppKit } from "@circle-fin/app-kit";

const kit = new AppKit();

const ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";

const balances = await kit.unifiedBalance.getBalances({
  sources: {
    address: ownerWalletAddress,
    chains: ["Base_Sepolia"],
  },
  networkType: "testnet",
  includePending: true,
});

console.dir(balances, { depth: null, colors: true });
You can check balances by address, adapter, chain, and network. See Check Unified Balance for more options.

4.2. Run the balance check script

In your terminal, run:
Shell
npm run balance
You’ll see output like:
Shell
{
  token: 'USDC',
  totalConfirmedBalance: '2.000000',
  breakdown: [
    {
      depositor: '0x...',
      totalConfirmed: '2.000000',
      breakdown: [
        {
          chain: 'Base_Sepolia',
          confirmedBalance: '2.000000',
          pendingBalance: '0.000000',
          pendingTransactions: []
        }
      ],
      totalPending: '0.000000'
    }
  ],
  totalPendingBalance: '0.000000'
}
After a deposit, funds can appear in totalPendingBalance before they are reflected in totalConfirmedBalance. Wait until the owner’s totalConfirmedBalance is high enough for the spend you plan to make before you continue.

Step 5. Spend from the owner’s balance

In this step, the delegate spends from the owner’s Unified Balance on Arc Testnet for the recipient. The Forwarding Service submits the destination mint, so you don’t need a wallet on Arc Testnet.
The Forwarding Service charges a fee that is deducted from the amount minted on the destination chain. The spend result includes the forwarding fee in the fee breakdown.

5.1. Create the spend script

Create a delegate-spend.ts file. This script spends 0.50 USDC from the owner’s Unified Balance on Arc Testnet for the recipient, signed by the delegate.
delegate-spend.ts
import { AppKit } from "@circle-fin/app-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

const SPEND_AMOUNT = "0.50";

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 ownerWalletAddress = "YOUR_BASE_SEPOLIA_OWNER_WALLET_ADDRESS";
const delegateWalletAddress = "YOUR_BASE_SEPOLIA_DELEGATE_WALLET_ADDRESS";
const recipientAddress = "YOUR_ARC_TESTNET_RECIPIENT_ADDRESS";

console.log(
  `Spending ${SPEND_AMOUNT} USDC on Arc_Testnet for ${recipientAddress}...\n`,
);

const result = await kit.unifiedBalance.spend({
  amount: SPEND_AMOUNT,
  token: "USDC",
  from: [
    {
      adapter,
      address: delegateWalletAddress,
      // Spend from the owner's balance; the delegate wallet signs.
      sourceAccount: ownerWalletAddress,
      allocations: [{ amount: SPEND_AMOUNT, chain: "Base_Sepolia" }],
    },
  ],
  to: {
    chain: "Arc_Testnet",
    recipientAddress,
    useForwarder: true,
  },
});

console.dir(result, { depth: null, colors: true });
You can also 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.

5.2. Run the spend script

In your terminal, run:
Shell
npm run spend
The script logs SDK events and prints the spend result. You’ll see output like:
Shell
{
  recipientAddress: '0x...',
  destinationChain: 'Arc_Testnet',
  txHash: '0x...',
  explorerUrl: 'https://testnet.arcscan.app/tx/0x...',
  allocations: [
    {
      amount: '0.5',
      chain: 'Base_Sepolia',
      sourceAccount: '0x...'
    }
  ],
  fees: [
    { type: 'provider', token: 'USDC', amount: '0.000025', ... },
    { type: 'gasFee', token: 'USDC', amount: '0.024137', ... },
    { type: 'forwarder', token: 'USDC', amount: '0.014162' }
  ],
  transferId: '...',
  expirationBlock: '...'
}

5.3. Verify the spend

Use the spend result to confirm that USDC arrived at the recipient address on Arc Testnet. When you use the Forwarding Service, the result can include a transferId instead of a locally submitted destination transaction hash. The received amount can be less than the requested spend after fees. For more on fees, see How Unified Balance fees work.