Skip to main content
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.