Featured for Wallets. Recover unused balances automatically. Canonical wallet-cleanup pattern.
Scenario
A user wants to swap 80% of a USDC balance to USDT but deliberately leave the remaining 20% untouched. In compose terms this is a core.split where only one of the two output handles is bound to a downstream node — the other is left dangling. Dangling resources stay on the per-signer proxy contract at the end of execution; sweepTo tells the backend to transfer them back to the sender (or any specified address) so nothing is stranded.
This recipe is the canonical shape for dust recovery — any pattern where part of an input is intentionally unused and must not be left on the proxy.
What this recipe demonstrates
core.split with bps: 8000 (80/20) and only one of the two output handles bound downstream.
- Implicit dust tracking — the unbound handle (
b) never becomes an input to another op; the compiler treats its balance as a terminal residual resource.
sweepTo: builder.context.sender returns all residual balances to the sender at the end of the transaction, including the deliberately unused 20%.
- A minimal two-node flow: one split, one swap.
Full example
Adapted from dustSweep.ts in the composer-sdk-examples repo.
import {
createComposeSdk,
materialisers,
resources,
} from '@lifi/composer-sdk';
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const sdk = createComposeSdk({ baseUrl: 'https://composer.li.quest' });
const builder = sdk.flow(1, {
name: 'dust-sweep',
inputs: {
amountIn: resources.erc20(USDC, 1),
},
});
// Split USDC 80/20.
// Only the 80% portion (`a`) is consumed by the swap.
// The 20% portion (`b`) is intentionally left unbound — it stays on
// the proxy as dust and gets swept back to the sender.
const { a } = builder.core.split('split', {
bind: { source: builder.inputs.amountIn },
config: { bps: 8000 },
});
// Swap only the 80% portion to USDT.
builder.lifi.swap('swap', {
bind: { amountIn: a },
config: {
resourceOut: resources.erc20(USDT, 1),
slippage: 0.03,
},
});
// sweepTo ensures the unused 20% USDC is transferred back to the
// sender rather than being stranded on the proxy.
// See /composer/composer-api/concepts/sweeping-and-amounts for the full semantics.
const result = await builder.compile({
signer: '0xYourSignerAddress',
inputs: {
amountIn: materialisers.directDeposit({ amount: '10000000' }), // 10 USDC
},
sweepTo: builder.context.sender,
});
What to observe
- Inputs. One resource input (
amountIn: USDC).
- Nodes. Two:
core.split then lifi.swap. Only split.a is referenced — split.b is never consumed.
- Dangling handle. Destructuring
const { a } = builder.core.split(...) quietly drops b from the TypeScript side; on the wire, split still declares both outputs and the compiler knows b is unused.
- Terminal resources. Two: the swapped USDT (the intentional output) and the unused USDC from
split.b (the dust). Both are swept to the sender by sweepTo.
sweepTo shape. request.run.sweepTo === { $ref: 'context.sender' } when you pass builder.context.sender. You can also pass a literal address string, as in the swap-to-recipient recipe.
producedResources[<name>].simulated?.amountOut. producedResources on the ComposeCompileResult lists both the USDT (from the swap) and the residual USDC amounts.