Validator Logo

dhozil

Crypto Blog & Insights

← Back to Blog

Unified Balance Kit Meets Streaming Payments — What Changes for Builders on Arc

Posted on April 28, 2026 • Tags: Unified Balance Kit StreamPay Tutorial USDC Arc TypeScript
Arc builder

Arc released the Unified Balance Kit four days ago. I've been integrating it with StreamPay — a real-time USDC streaming protocol I built on Arc testnet. Here's what changed, what it unlocked, and the code patterns that actually work.


What StreamPay Does, and the Problem It Had

StreamPay is a payment streaming protocol I deployed on Arc testnet: lock USDC, it flows per second to a recipient, they withdraw anytime. Contract address: 0x46937C3663...a54B.

The original version has one friction point for real use: funding the stream requires the user to already have USDC on Arc testnet. If they have USDC on Ethereum Sepolia or Avalanche Fuji, they need to bridge first, then approve, then create the stream — three separate steps across two different interfaces before a single dollar flows.

The Unified Balance Kit changes this entirely. Instead of managing a chain-specific USDC balance, a user can hold a unified USDC balance — funded from any supported chain — and spend it directly into StreamPay on Arc. One balance, one approval flow, one stream creation.

What Unified Balance Kit actually is

A TypeScript SDK that wraps Circle Gateway's unified-balance model. One interface for deposit, getBalances, estimateSpend, and spend across supported chains — without building custom multichain routing logic.


The New Flow: Deposit Anywhere, Stream on Arc

Here's what the updated flow looks like. The key insight is that Gateway's unified balance acts as a funding layer sitting above the chain-specific escrow in StreamPay.

1
User deposits USDC from any supported chain
kit.deposit() — funds Gateway Wallet contract, credits unified balance. Works from Ethereum Sepolia, Avalanche Fuji, Base Sepolia, or Arc Testnet.
kit.deposit() · UnifiedBalanceKit
2
Check unified balance across all chains
kit.getBalances() returns a chain-agnostic view. Show user their available USDC without chain switching.
kit.getBalances() · includePending: true
3
Estimate fees before committing
kit.estimateSpend() simulates the spend offchain. User sees exact fees before signing — no surprises.
kit.estimateSpend() · Preview before tx
4
Spend from unified balance → fund StreamPay escrow on Arc
kit.spend() routes from unified balance to Arc. StreamPay's createStream() receives USDC, stream begins. Sub-second finality — recipient earns before next second ticks.
kit.spend() → createStream() · Arc sub-second finality

Implementation — Setting Up the Kit

First, install the SDK. Unified Balance Kit is part of Arc App Kits:

npm install @circle-fin/unified-balance-kit
npm install @circle-fin/developer-controlled-wallets

Initialize the kit with an EVM adapter pointing at Arc Testnet:

import { UnifiedBalanceKit, EvmAdapter } from '@circle-fin/unified-balance-kit';
import { initiateUserControlledWalletsClient } from '@circle-fin/user-controlled-wallets';

// Arc Testnet RPC — USDC is the native gas token here
const ARC_RPC = 'https://rpc.testnet.arc.network';
const ARC_CHAIN_ID = 5042002;

// Initialize EVM adapter for Arc
const evmAdapter = new EvmAdapter({
  rpcUrl: ARC_RPC,
  chainId: ARC_CHAIN_ID,
});

// Initialize the kit
const kit = new UnifiedBalanceKit({
  apiKey: process.env.CIRCLE_API_KEY,
  adapters: [evmAdapter],
});

Checking Balances Across All Chains at Once

Before StreamPay, the user experience required knowing which chain their USDC was on. With getBalances, that disappears:

async function getUnifiedBalance(walletAddress: string) {
  const balances = await kit.getBalances({
    sources: { account: walletAddress },
    networkType: 'testnet',
    includePending: true, // include deposits still finalizing
  });

  // Returns chain-agnostic unified total
  // No need to query Arc, Sepolia, Fuji separately
  console.log(`Unified USDC: ${balances.unified.amount}`);
  console.log(`On Arc: ${balances.chains['Arc_Testnet']?.amount ?? '0'}`);

  return balances;
}

Spending Into StreamPay — the Key Integration

This is the pattern that eliminates the bridge-first requirement. The user has USDC in their unified balance; we route it to Arc and fund the stream escrow in one flow:

import { ethers } from 'ethers';

const STREAMPAY_ADDR = '0x46937C3663101b3fE7F282A49F397d1f5C17a54B';
const USDC_ARC     = '0x3600000000000000000000000000000000000000';

// Step 1: Estimate fees before committing
async function estimateStreamFunding(
  walletAddress: string,
  amountUsdc: string // e.g. "10.00"
) {
  const estimate = await kit.estimateSpend({
    amount: amountUsdc,
    from: {
      adapter: evmAdapter,
      allocations: { amount: amountUsdc, chain: 'Arc_Testnet' },
    },
    to: {
      adapter: evmAdapter,
      chain: 'Arc_Testnet',
      recipientAddress: STREAMPAY_ADDR, // StreamPay escrow
    },
    token: 'USDC',
  });

  // Show user: amount + estimated fees before they sign
  return {
    amount: estimate.amount,
    fees: estimate.fees,
    total: estimate.totalCost,
  };
}

// Step 2: Spend from unified balance → StreamPay escrow
async function fundAndCreateStream(
  walletAddress: string,
  recipient:     string,
  amountUsdc:    string,
  durationSecs:  number,
  label:         string
) {
  // 1. Route USDC from unified balance to StreamPay on Arc
  const spendResult = await kit.spend({
    amount: amountUsdc,
    from: {
      adapter: evmAdapter,
      allocations: { amount: amountUsdc, chain: 'Arc_Testnet' },
    },
    to: {
      adapter: evmAdapter,
      chain: 'Arc_Testnet',
      recipientAddress: STREAMPAY_ADDR,
    },
    token: 'USDC',
  });

  // 2. Call createStream() on StreamPay contract
  // USDC already in escrow — Arc finalizes in <1 second
  const provider = new ethers.JsonRpcProvider(ARC_RPC);
  const signer   = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

  const streamPay = new ethers.Contract(STREAMPAY_ADDR, [
    'function createStream(address,uint256,uint256,string) returns (uint256)',
  ], signer);

  const amountRaw = ethers.parseUnits(amountUsdc, 6); // 6-decimal ERC-20
  const tx = await streamPay.createStream(
    recipient, amountRaw, BigInt(durationSecs), label
  );

  const receipt = await tx.wait();
  console.log(`Stream live. Tx: ${receipt.hash}`);
  return receipt;
}

Decimal context — avoid the common mistake

Unified Balance Kit amounts use human-readable strings like "10.00". StreamPay's createStream() expects 6-decimal raw integers (10_000_000 for 10 USDC). Always convert with ethers.parseUnits(amount, 6) before passing to the contract — never to parseEther.


What This Changes for the SEA Use Case

I've been writing about the Southeast Asia remittance angle — 60M+ gig workers waiting 24–48 hours for platform payouts. The original StreamPay required users to have USDC specifically on Arc testnet, which is a friction point for adoption.

With Unified Balance Kit, the funding chain becomes invisible to the end user. A Grab driver in Medan with USDC on any supported chain can fund a salary stream to a family member — the SDK handles routing, the user just sees their total balance.

Scenario Before UBK After UBK
User has USDC on Ethereum Sepolia Bridge → approve → stream (3 steps, 2 UIs) deposit() → spend() → stream (1 SDK, 1 flow)
Check available balance Query each chain separately getBalances() returns unified total
Fee estimate before signing Guess from gas estimators estimateSpend() — exact preview offchain
Multi-chain USDC consolidation Manual CCTP + routing logic Kit handles abstraction automatically

What I'm Still Figuring Out

The integration is working on testnet, but there are two open questions I'd love input on from other builders:

1. Pending balance UX. When a user deposits from a source chain, there's a finalization window before the unified balance is credited. includePending: true returns the pending amount, but how are other builders handling the in-between state in their UI — showing a spinner, a countdown, or something else?

2. Partial allocation. The spend() call lets you specify allocations per chain within the unified balance. For StreamPay, I'm funding from Arc only — but for cross-corridor streams (e.g., sender on Sepolia, recipient cashes out on Arc), what's the optimal allocation strategy?

If you're building with Unified Balance Kit and have run into either of these, let me know in the comments. Happy to share the full StreamPay + UBK integration code as a reference repo.


Community contribution from Medan, Indonesia — April 2026