Unified Balance Kit: How Auto-Allocation Picks Which Chain Balances spend( ) Uses
-Auto-Allocations-work-in-Unified-Balance_1200x630.jpg)
Summary
When you omit from.allocations on spend(), Unified Balance Kit sources USDC from your unified balance using a greedy strategy. This post walks through balance discovery, chain ordering, phased allocation, and burn-intent construction end to end.
When using Unified Balance Kit to make withdrawals from your unified USDC balance, omitting from.allocations lets the SDK auto-pick sources.
This makes sense for use-cases where you care more about the total amount spent than the chains the funds came from. For example, Relay uses this pattern when settling crosschain payments from one Gateway USDC balance instead of pre-positioned inventory on every chain.
Auto-allocation makes use of a greedy strategy to determine which funds to pull from, and we will be going in-depth on how that works in this post.
Implementing auto-allocations
When using explicit allocations, if you include an allocations value for one of the sources, then you must do so for all the other sources listed. And make sure that all the allocations add up to the same value used in the amount parameter. Auto-allocation kicks in when you omit allocations altogether and computes the split from amount.
App Kits encompasses all the various stand-alone kits and supports a variety of adapter setups as well so you can pick whichever approach best suits your project. You don’t need a lot of code to spend from your unified USDC balance with Unified Balance Kit, this example shows the implementation with a Circle Wallets adapter.
const kit = new AppKit();
const adapter = createCircleWalletsAdapter({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
const depositorAddress = "0x1234SourceDepositorWalletAddress12345678";
const result = await kit.unifiedBalance.spend({
amount: "100",
token: "USDC",
from: {
adapter,
address: depositorAddress
},
to: {
adapter,
chain: "Arc_Testnet",
address: depositorAddress,
},
});
How spend() Auto-Allocation Works Step by Step
Let’s have a scenario where you have USDC spread across Arc, Avalanche, Base and Ethereum sources in your Gateway balance. And you want to spend 100 USDC from this unified balance without explicit allocations.
Step 1: Discover balances
For each entry in from, the kit calls getBalances and builds a pool from confirmed per-chain balances only. This means pending deposits (usually when depositing from slower finality chains), are not considered spendable here. If you call getBalances yourself, you will see a result similar to this:
{
token: "USDC",
totalConfirmedBalance: "175.001000",
breakdown: [
{
depositor: "0x1234SourceDepositorWalletAddress12345678",
totalConfirmed: "175.001000",
breakdown: [
{
chain: "Ethereum_Sepolia",
confirmedBalance: "30.000000",
}, {
chain: "Base_Sepolia",
confirmedBalance: "40.000000",
}, {
chain: "Avalanche_Fuji",
confirmedBalance: "25.000000",
}, {
chain: "Arbitrum_Sepolia",
confirmedBalance: "0.000000",
}, {
chain: "Sonic_Testnet",
confirmedBalance: "0.000000",
}, {
chain: "World_Chain_Sepolia",
confirmedBalance: "0.000000",
}, {
chain: "Sei_Testnet",
confirmedBalance: "0.000000",
}, {
chain: "HyperEVM_Testnet",
confirmedBalance: "0.000000",
}, {
chain: "Arc_Testnet",
confirmedBalance: "80.001000",
}, {
chain: "Optimism_Sepolia",
confirmedBalance: "0.000000",
}, {
chain: "Polygon_Amoy_Testnet",
confirmedBalance: "0.000000",
}, {
chain: "Unichain_Sepolia",
confirmedBalance: "0.000000",
}
],
}
],
}
To make this easier to visualize, the breakdown is as follows:
What’s important here is that totalConfirmedBalance: "175.001000" indicates we have more than enough for a 100 USDC spend.
Step 2: Order chains
Before any allocations take place, the chains are sorted into a fixed priority list.
For the chains that fall into the priority 2 group (non-Ethereum chains), they are greedy-ordered by balance, where chains with the largest balance are drawn from first. For our example, the ordering is as follows:
Step 3: Allocate in phases
An internal-only computeAutoAllocation function runs up to three passes over the ordered list of chains. The main phase allocates the amount needed across the chains on the list. For each chain draw, the kit will take into account:
- A Gas buffer
- A 0.5bps transfer fee (for crosschain draws only)
- The Forwarder fee (if
useForwarder: truewas set in the spend parameters)
If the confirmed balance (after fee reserves) cannot cover the requested amount, the kit will throw a KitError stating BALANCE_INSUFFICIENT_TOKEN. This will happen if you call estimateSpend() or spend(). The next 2 passes are only relevant if config.customFee is set. So if you have not set any custom fees, the allocator stops here once the first pass is complete.
The second pass is the draw for developer fees. The kit splits customFee.value into a developer share (90% by default) and a Circle share (10%). This pass allocates the developer’s share of the custom fee across the remaining balances, according to the same ordered list. The third pass sources Circle’s share from whatever capacity remains after the second pass, with the same chain ordering.
For our example, we will only walk through the first pass:
The result of this walk is that 80 USDC will be drawn from the Arc source balance, and the remaining 20 USDC from the Base source balance.
Step 4: Build burn intents
Each allocation becomes a separate burn intent. For our example with no fees, we will end up with 2 burn intents.
Each intent is a BurnIntent with a TransferSpec that contains everything about a transfer from one domain to another, like source and destination domains, contracts, depositor, signer, value etc.
The SDK serializes the transfer specs and calls POST /v1/estimate. For EVM chains, multiple intents are batched into one { intents: [...] } payload, while on Solana, it is one spec per intent. Gateway returns per-intent maxFee and maxBlockHeight which is required to build the burn intent for signing.
The estimate is part of the flow regardless of whether you call estimateSpend() or not, but calling estimateSpend() allows you to also see what the fee estimates are. It will look something like this:
{
fees: [
{
type: "provider",
token: "USDC",
amount: "0.001",
allocations: [
{
chain: "Base_Sepolia",
amount: "0.001",
}
],
}, {
type: "gasFee",
token: "USDC",
amount: "0.01385",
allocations: [
{
chain: "Arc_Testnet",
amount: "0.00385",
}, {
chain: "Base_Sepolia",
amount: "0.01",
}
],
}
],
}
From here we can see that only Base has a provider fee of USDC 0.001 because it is a crosschain draw, and the gas fees are USDC 0.01385, with USDC 0.00385 from Arc and USDC 0.01 from Base.
Step 5. Sign and execute spend
The SDK groups estimated intents by source adapter. EVM intents from the same adapter are signed in one EIP-712 batch, whereas Solana signs one intent at a time.
For a batched EVM spend, the signer authorizes a BurnIntentSet in one EIP-712 operation. Gateway validates that signature and returns a single attestation and signature covering every intent in the set. The destination adapter submits one gatewayMint call with that attestation and USDC for all source draws lands in one mint transaction on the destination chain.
Start building with Unified Balance Kit
Unified Balance Kit’s auto-allocations use a cost-aware strategy, letting developers skip having to explicitly calculate the amount drawn from multiple source balances if fine-grained control around fixed chain splits or reconciliation rules are not applicable for their use case.
To learn more, do check out our developer documentation for Gateway and Unified Balance Kit. And join discussions with our developer community on Discord and Arc House.
Circle Technology Services, LLC (“CTS”) is a software provider and does not provide regulated financial or advisory services. You are solely responsible for services you provide to users, including obtaining any necessary licenses or approvals and otherwise complying with applicable laws. For additional details, please click here to see the Circle Developer terms of service.
USDC is issued by regulated affiliates of Circle. See Circle’s list of regulatory authorizations.
Circle has a commercial relationship with Relay.

