How to sweep token balances to treasury

Sweeping consolidates a specific asset from many source wallets into a single destination wallet - a common pattern for treasury management, gas cost reduction, and omnibus architecture maintenance. This page walks through the full API sequence: querying balances across the vault, filtering eligible wallets, and initiating sponsored transfers in bulk.

Prerequisites

RequirementWhere to set it up
A vault with a service account (signer role)Service accounts guide
A designated destination (treasury) walletConsole — create a wallet
A sponsor wallet funded with native gas tokens on the target chainSee Set up and maintain a gas sponsor wallet
Co-Signer running in sign-all or webhook policy mode for automated signingCo-Signer guide
Sponsored transfers enabled for your vault on the target chainContact Utila if sponsor is not accepted; see Supported chains below

If your target chain does not support sponsored transfers (e.g. Bitcoin, Cosmos, TON), see the Non-sponsored chains section at the bottom of this page.

Overview of the sequence

1. Query balances for the target asset across all wallets (single call)
2. Filter: remove zero-balance, below-threshold, destination, and sponsor wallets
3. Initiate one sponsored transfer per eligible wallet
4. Track each transaction to CONFIRMED

Step 1 — Query balances for the target asset

Use the wildcard wallet ID (-) to query a specific asset's balance across every wallet in the vault in a single call. This avoids N+1 requests.

POST /v2/vaults/{vault_id}/wallets/-:queryBalances
{
  "filter": "asset(\"assets/erc20.polygon-mainnet.0x3c499c542cef5e3811e1192ce70d8cc03d5c3359\")"
}

Response:

{
  "balances": [
    {
      "asset": "assets/erc20.polygon-mainnet.0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
      "wallet": "vaults/abc123/wallets/w001",
      "value": "15.00",
      "rawValue": "15000000",
      "usdValue": "15.00"
    },
    {
      "asset": "assets/erc20.polygon-mainnet.0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
      "wallet": "vaults/abc123/wallets/w002",
      "value": "0.50",
      "rawValue": "500000",
      "usdValue": "0.50"
    }
  ]
}

No filter: Omit the filter body entirely to get all asset balances across all wallets in one call. Useful if you're sweeping multiple assets or want a full balance snapshot first.

Pagination: Default page size is 50. Use pageSize=1000 (max) and follow nextPageToken to iterate all results.

Amount fields:

FieldUnitExample
valueHuman-readable"1.5" = 1.5 USDC
rawValueSmallest unit"1500000" = 1.5 USDC (6 decimals)

Use value when building your eligible wallet list and when passing amount to InitiateTransaction.

Step 2 - Filter eligible wallets

From the balance results, build the list of wallets to sweep. Apply these filters in order:

  1. Balance > 0 - skip wallets with no holdings of the target asset
  2. Threshold (optional) - only sweep wallets above a minimum value to avoid dust
  3. Frozen balance - if frozenValue > 0, sweep only the non-frozen portion (value minus frozenValue); skip entirely if nothing is available
  4. Exclude the destination wallet - don't sweep funds from the treasury to itself
  5. Exclude the sponsor wallet - don't sweep the gas wallet

You don't need to resolve blockchain addresses separately. The wallet resource name (vaults/{vault_id}/wallets/{wallet_id}) works directly as source in the next step.

Step 3 - Initiate sponsored transfers

For each eligible wallet, call InitiateTransaction with the sponsor field set to your gas wallet.

POST /v2/vaults/{vault_id}/transactions:initiate
{
  "details": {
    "assetTransfer": {
      "asset": "assets/erc20.polygon-mainnet.0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
      "amount": "15.00",
      "source": "vaults/abc123/wallets/w001",
      "destination": "vaults/abc123/wallets/treasury",
      "sponsor": "vaults/abc123/wallets/gas-sponsor"
    }
  }
}

When sponsor is set, the source wallet needs no native gas tokens - only the asset being transferred.

Repeat for each eligible wallet, substituting source and amount from your filtered results. Each call returns immediately with state: CREATED; transactions are processed asynchronously.

If one wallet fails, continue with the rest - log the error and the wallet ID, don't abort the sweep.

Field reference

asset - Full asset resource name with assets/ prefix.

amount - Human-readable units. Pass the value field from the balance response directly.

"amount": "15.00"   ← 15.00 USDC
"amount": "0.50"    ← 0.50 USDC

source and destination - three accepted formats:

FormatExample
Wallet resource name"vaults/abc123/wallets/w001"
Raw blockchain address"0x2A9E89409A3E..."
Specific wallet address"vaults/abc123/wallets/w001/addresses/addr001"

Wallet resource name is recommended - it avoids having to resolve addresses separately and works across networks.

sponsor - wallet resource name of the gas wallet:

"sponsor": "vaults/{vault_id}/wallets/{sponsor_wallet_id}"

EVM - delegation prerequisite

On EVM chains, if a source wallet has not previously delegated to Utila's EIP-7702 contract, a single InitiateTransaction call may produce two transactions: a delegation transaction (EVM_ACCOUNT_DELEGATION) followed by the transfer. Both are returned in the response. Track both IDs and wait for the delegation to reach CONFIRMED before the transfer can execute.

Supported chains

Sponsored transfers (sponsor on assetTransfer) are supported on:

Chain familyNetworks
EVMEthereum, Polygon, Base, Arbitrum, Optimism, Avalanche, BSC, Linea, Scroll, and other EIP-7702-enabled networks
SolanaSolana mainnet / devnet
SuiSui mainnet / testnet
AptosAptos mainnet / testnet

Sponsorship must be enabled for your vault on the target chain. If InitiateTransaction rejects a request that includes sponsor, contact Utila Support to enable sponsored transfers for that chain.

Step 4 - Track results

Each InitiateTransaction call returns a transaction resource name. Collect all of them.

GET /v2/vaults/{vault_id}/transactions/{transaction_id}

Happy path:

AWAITING_POLICY_CHECK → AWAITING_SIGNATURE → SIGNED → AWAITING_PUBLISH → PUBLISHED → MINED → CONFIRMED

Policy-gated vaults may include AWAITING_APPROVAL before signing.

Failure and end states:

StateTerminal?Meaning
MINED_FAILEDNoIncluded in a block but failed on-chain (e.g. reverted). Continues to FAILED after confirmations.
FAILEDYesFailure confirmed by subsequent blocks. Inspect the on-chain reason before retrying.
DECLINEDYesRejected by policy.
DROPPEDYesDropped from the mempool without being mined - usually safe to retry.
CANCELEDYesCanceled by the initiator or an admin.
REPLACEDYesReplaced by another transaction (same nonce / RBF).
EXPIREDYesTimed out.

For production sweeps, subscribe to webhooks (TRANSACTION_STATE_UPDATED) instead of polling each transaction individually.

Error handling

ScenarioHow to handle
Wallet has 0 balanceSkip
frozenValue > 0Sweep available portion only; skip if all frozen
Sponsor has insufficient gasCheck sponsor balance before starting; abort with clear message
InitiateTransaction returns errorLog wallet + error; continue with remaining wallets
Stuck at AWAITING_SIGNATURECo-Signer not running or service account lacks signer role
Stuck at AWAITING_APPROVALPolicy requires manual votes - check vault policy rules
MINED_FAILEDNon-terminal - will continue to FAILED after confirmations
EVM: delegation + transferTrack both transaction IDs; delegation must confirm first
Pagination neededFollow nextPageToken in wallet list and balance responses

Common asset IDs

AssetNetworkAsset ID
USDCEthereumerc20.ethereum-mainnet.0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
USDTEthereumerc20.ethereum-mainnet.0xdac17f958d2ee523a2206206994597c13d831ec7
USDCPolygonerc20.polygon-mainnet.0x3c499c542cef5e3811e1192ce70d8cc03d5c3359
USDCBaseerc20.base-mainnet.0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
USDCArbitrumerc20.arbitrum-mainnet.0xaf88d065e77c8cc2239327c5edb3a432268e5831
USDCSolanaspl.solana-mainnet.EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
ETHEthereumnative.ethereum-mainnet
MATICPolygonnative.polygon-mainnet
SOLSolananative.solana-mainnet

Full asset names use the assets/ prefix: assets/erc20.polygon-mainnet.0x3c49...

Non-sponsored chains

For chains that don't support the sponsor parameter (Bitcoin, Cosmos, TON), use a three-step gas pre-funding flow.

1. Estimate gas per wallet

POST /v2/vaults/{vault_id}/transactions:estimateFee

Same body structure as InitiateTransaction - details.assetTransfer with asset, amount, source, destination, no sponsor.

2. Send native gas to each source wallet

Use the estimated fee plus a 10–20% buffer as amount:

{
  "details": {
    "assetTransfer": {
      "asset": "assets/native.{network}",
      "amount": "{estimated_gas_with_buffer}",
      "source": "vaults/{vault_id}/wallets/{gas_wallet_id}",
      "destination": "vaults/{vault_id}/wallets/{source_wallet_id}"
    }
  }
}

Wait for this transfer to reach CONFIRMED before proceeding. If you initiate the asset transfer before gas arrives on-chain, it will fail.

3. Initiate the asset transfer

Same as Step 3 above, without the sponsor field.

Fee estimate staleness: Network fees fluctuate. If significant time passes between your estimateFee call and the gas top-up, or between the top-up and the asset transfer, the actual fee at execution may exceed your estimate. Run estimate → gas top-up → asset transfer as a tight sequence per wallet, and add a buffer.

Bitcoin: Use wallet resource names for source and destination - raw addresses are not supported as source.