Skip to main content

Scenario

A user holds WETH and wants yield-bearing Aave USDC (aEthUSDC) on Ethereum mainnet. Two operations are needed: a swap to convert WETH into USDC, and a zap to deposit that USDC into Aave. The compose stack expresses this as a two-node Flow where the swap’s amountOut handle is threaded directly into the zap’s amountIn binding — no manual bookkeeping, no intermediate transfer. This recipe is the canonical shape for chained swap → zap flows: a routing-provider swap followed by a protocol-specific zap, both lowered to VM instructions by the backend.

What this recipe demonstrates

  • Threading an op’s typed output handle into a downstream op’s input (swapOutputs.amountOutzap.amountIn).
  • Running a lifi.swap without a slippage guard (the swap’s amountOut port carries providesMinimum, so slippage is enforced by the provider).
  • Attaching a slippage guard on the lifi.zap output (see the guards concept page).
  • Using materialisers.directDeposit to fund the flow with a fixed amount, and sweepTo: builder.context.sender to return any residual tokens.

Full example

Adapted from swapAndZap.ts in the composer-sdk-examples repo.
import {
  createComposeSdk,
  guards,
  materialisers,
  resources,
} from '@lifi/composer-sdk';

const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
// Aave aEthUSDC receipt token on Ethereum mainnet
const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';

const sdk = createComposeSdk({
  baseUrl: 'https://composer.li.quest',
  apiKey: process.env.LIFI_API_KEY,
});

// Declare the flow with a single WETH input on Ethereum mainnet.
const builder = sdk.flow(1, {
  name: 'swap-and-zap-weth-to-aave',
  inputs: {
    amountIn: resources.erc20(WETH, 1),
  },
});

// Swap WETH → USDC via LI.FI.
const swapOutputs = builder.lifi.swap('swap', {
  bind: { amountIn: builder.inputs.amountIn },
  config: {
    resourceOut: resources.erc20(USDC, 1),
    slippage: 0.03,
  },
});

// Zap the swapped USDC into Aave's aEthUSDC position via LI.FI.
// The swap's amountOut handle is threaded directly into the zap's amountIn.
// See /composer/composer-api/concepts/ref-grammar for how handles translate to refs.
builder.lifi.zap('zap', {
  bind: { amountIn: swapOutputs.amountOut },
  config: {
    resourceOut: resources.erc20(A_ETH_USDC, 1),
  },
  guards: [guards.slippage({ port: 'amountOut', bps: 100 })],
});

const result = await builder.compile({
  signer: '0xYourSignerAddress',
  inputs: {
    amountIn: materialisers.directDeposit({
      amount: '1000000000000000000', // 1 WETH (18 decimals)
    }),
  },
  sweepTo: builder.context.sender,
});

What to observe

  • Inputs. One resource input (amountIn: WETH). The directDeposit materialiser transfers exactly 10^18 wei (1 WETH) into the VM; no balance read is performed.
  • Nodes. Exactly two: flow.nodes[0].op === 'lifi.swap', flow.nodes[1].op === 'lifi.zap'.
  • Handle threading. The zap’s bind.amountIn is the ref swap.amountOut — no intermediate resource or transfer.
  • Guards. Only the zap carries a slippage guard (port: 'amountOut', bps: 100, i.e. 1%). The swap has no guard because its amountOut port already provides a minimum.
  • Terminal resource. The Aave aEthUSDC produced by the zap is the terminal resource and is swept to the sender.
  • producedResources[<name>].simulated?.amountOut. On the ComposeCompileResult, producedResources includes the zap’s output with the simulated aEthUSDC amount at .simulated?.amountOut.