lifi.swap is one of many ops the Composer supports. It has a dedicated page here because it’s worth explaining in depth — for the complete, live list of every op, see the Op Catalog.
lifi.swap is the primary cross-token primitive in a flow. It consumes a resource on the amountIn port and produces a new resource of the resourceOut token on the amountOut port, using LI.FI’s aggregator to pick the best route across DEXes and bridges. Because the aggregator already returns a minimum-out guarantee (providesMinimum), you usually do not need a separate slippage guard — pass slippage in the config instead and the minimum is baked into the quote.
Use lifi.swap for any on-chain or cross-chain swap that terminates in a standard ERC-20. For deposits into a vault or lending market, prefer lifi.zap, which additionally chains a routing-edge deposit after the swap.
Understanding unspentIn
lifi.swap requests a concrete quote from the LI.FI aggregator when you compile the flow and pins the quoted input amount. The on-chain swap spends exactly that pinned amount — no more, no less.
If the runtime amount that actually flows into amountIn is larger than the pinned amount, the difference surfaces on a second output port, unspentIn, as a first-class linear resource of the input token. This typically happens when the amountIn value is only fully known at runtime — for example, when it comes from an upstream op whose output differs from the preview used to derive the quote, or when the input is simulation-resolved. If the actual amountIn is less than the pinned amount, the transaction reverts.
Semantics of unspentIn:
- Same token as
amountIn. The leftover keeps the input resource; it is not swapped or burned.
- Exact partition.
unspentIn = amountIn minus the amount consumed by the quoted swap. The op ignores any pre-existing dust on the execution address — it only accounts for the input that was handed in.
- Fully composable. Treat it like any other linear resource output: bind it to a downstream node (merge, swap it again, sweep it to a recipient) and it does not appear as a terminal.
- Omitted when zero. If the runtime amount exactly matches the quote,
unspentIn is suppressed from producedResources rather than showing up as a noisy zero terminal.
- Input and output must differ. Configuring
resourceOut to the same resource as amountIn is a validation error — use a no-op node or skip the swap instead.
amountOut is reported as the delta produced by the swap (post-call balance minus pre-call balance), not the total post-call balance. This matches “what this swap produced” even when the execution address already holds some of the output token.
Example
Swap WETH → USDC on Ethereum mainnet:
const builder = sdk.flow(1, {
name: 'swap-weth-to-usdc',
inputs: { amountIn: resources.erc20(WETH, 1) },
});
builder.lifi.swap('swap', {
bind: { amountIn: builder.inputs.amountIn },
config: {
resourceOut: resources.erc20(USDC, 1),
slippage: 0.03,
},
});
Full runnable recipe: Swap and zap.