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
| Requirement | Where to set it up |
|---|---|
| A vault with a service account (signer role) | Service accounts guide |
| A designated destination (treasury) wallet | Console — create a wallet |
| A sponsor wallet funded with native gas tokens on the target chain | See Set up and maintain a gas sponsor wallet |
| Co-Signer running in sign-all or webhook policy mode for automated signing | Co-Signer guide |
| Sponsored transfers enabled for your vault on the target chain | Contact 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:
| Field | Unit | Example |
|---|---|---|
value | Human-readable | "1.5" = 1.5 USDC |
rawValue | Smallest 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:
- Balance > 0 - skip wallets with no holdings of the target asset
- Threshold (optional) - only sweep wallets above a minimum
valueto avoid dust - Frozen balance - if
frozenValue> 0, sweep only the non-frozen portion (valueminusfrozenValue); skip entirely if nothing is available - Exclude the destination wallet - don't sweep funds from the treasury to itself
- 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:
| Format | Example |
|---|---|
| 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 family | Networks |
|---|---|
| EVM | Ethereum, Polygon, Base, Arbitrum, Optimism, Avalanche, BSC, Linea, Scroll, and other EIP-7702-enabled networks |
| Solana | Solana mainnet / devnet |
| Sui | Sui mainnet / testnet |
| Aptos | Aptos 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:
| State | Terminal? | Meaning |
|---|---|---|
MINED_FAILED | No | Included in a block but failed on-chain (e.g. reverted). Continues to FAILED after confirmations. |
FAILED | Yes | Failure confirmed by subsequent blocks. Inspect the on-chain reason before retrying. |
DECLINED | Yes | Rejected by policy. |
DROPPED | Yes | Dropped from the mempool without being mined - usually safe to retry. |
CANCELED | Yes | Canceled by the initiator or an admin. |
REPLACED | Yes | Replaced by another transaction (same nonce / RBF). |
EXPIRED | Yes | Timed out. |
For production sweeps, subscribe to webhooks (TRANSACTION_STATE_UPDATED) instead of polling each transaction individually.
Error handling
| Scenario | How to handle |
|---|---|
| Wallet has 0 balance | Skip |
frozenValue > 0 | Sweep available portion only; skip if all frozen |
| Sponsor has insufficient gas | Check sponsor balance before starting; abort with clear message |
InitiateTransaction returns error | Log wallet + error; continue with remaining wallets |
Stuck at AWAITING_SIGNATURE | Co-Signer not running or service account lacks signer role |
Stuck at AWAITING_APPROVAL | Policy requires manual votes - check vault policy rules |
MINED_FAILED | Non-terminal - will continue to FAILED after confirmations |
| EVM: delegation + transfer | Track both transaction IDs; delegation must confirm first |
| Pagination needed | Follow nextPageToken in wallet list and balance responses |
Common asset IDs
| Asset | Network | Asset ID |
|---|---|---|
| USDC | Ethereum | erc20.ethereum-mainnet.0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 |
| USDT | Ethereum | erc20.ethereum-mainnet.0xdac17f958d2ee523a2206206994597c13d831ec7 |
| USDC | Polygon | erc20.polygon-mainnet.0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 |
| USDC | Base | erc20.base-mainnet.0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 |
| USDC | Arbitrum | erc20.arbitrum-mainnet.0xaf88d065e77c8cc2239327c5edb3a432268e5831 |
| USDC | Solana | spl.solana-mainnet.EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| ETH | Ethereum | native.ethereum-mainnet |
| MATIC | Polygon | native.polygon-mainnet |
| SOL | Solana | native.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
sourceanddestination- raw addresses are not supported assource.