> ## Documentation Index
> Fetch the complete documentation index at: https://anypay-edge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Composable Actions

> Chain multiple DeFi steps into a single cross-chain intent using the composable actions builders.

## Composable Actions with Trails

Composable Actions allow developers to easily specify multiple actions to take on a destination chain that can compose with multiple protocols. For example, staking in a liquid staking token and then depositing that token into a vault. This means a user can sign a single transaction and multiple onchain interactions can take place from virtually any chain as the starting point.

Instead of encoding calldata by hand, you build a list of high-level steps — `swap`, `lend`, `deposit`, `assertCondition`, `custom` — and pass them to `useQuote` or `useTrailsSendTransaction`. Trails handles quoting, bridging, and executing.

## Use Cases

Ideal for a variety of multi-step DeFi scenarios:

* Stake a token and deposit the staked derivative into a lending protocol in one transaction
* Swap to a vault asset and deposit, starting from any token on any chain
* Split funds across multiple protocols atomically — if any step fails, the whole batch reverts
* Chain yield strategies across chains without requiring the user to touch intermediate steps

For a deeper look at the protocol model, see [Composable Actions Overview](/sdk/composable-actions/overview). For the full builder API, see [Building Actions](/sdk/composable-actions/building-actions).

Both hooks accept human-readable decimal strings for token amounts. For example, pass `"100"` for 100 USDC, not `"100000000"`.

## Supply into a lending protocol

Use `lend` to supply into supported lending markets such as Aave. Pass a `marketId` from [`useEarnMarkets`](/sdk/composable-actions/markets-and-providers):

```tsx theme={null}
import { useTrailsSendTransaction, lend, erc20Utils } from '0xtrails'

export function LendButton({ recipient }: { recipient: `0x${string}` }) {
  const { sendTransaction, isPending } = useTrailsSendTransaction({
    actions: [
      lend({
        marketId: 'base-usdc-aave-v3-lending',
        amount: '100',
      }),
    ],
  })

  return (
    <button
      disabled={isPending}
      onClick={() =>
        sendTransaction({
          to: recipient,
          tokenAddress: erc20Utils.USDC.addressOn('base'),
          tokenAmount: '100', // human-readable USDC amount
        })
      }
    >
      {isPending ? 'Sending...' : 'Supply 100 USDC to Aave'}
    </button>
  )
}
```

The user selects what token and chain to pay from. Trails bridges and swaps to deliver 100 USDC on Base, then calls `supply()` on the Aave pool.

## Deposit into a vault

Use `deposit` for ERC-4626 and vault-style markets (Morpho, Yearn, SummerFi, Sky):

```tsx theme={null}
import { useTrailsSendTransaction, deposit, erc20Utils } from '0xtrails'

export function MorphoDepositButton({ recipient }: { recipient: `0x${string}` }) {
  const { sendTransaction, isPending } = useTrailsSendTransaction({
    actions: [
      deposit({
        marketId: 'base-usdc-pusdc-0x1401d1271c47648ac70cbcdfa3776d4a87ce006b-4626-vault',
        amount: '100',
      }),
    ],
  })

  return (
    <button
      disabled={isPending}
      onClick={() =>
        sendTransaction({
          to: recipient,
          tokenAddress: erc20Utils.USDC.addressOn('base'),
          tokenAmount: '100',
        })
      }
    >
      {isPending ? 'Sending...' : 'Deposit 100 USDC to Morpho'}
    </button>
  )
}
```

Vault shares land on the user's wallet by default. Pass `receiverAddress` to redirect them.

## Chain multiple DeFi steps

Use `dynamic()` to consume whatever the previous step produced without predicting bridge fees or slippage. This example delivers 0.2 USDT on Polygon, splits it across four destination steps:

```tsx theme={null}
import {
  useTrailsSendTransaction,
  deposit,
  swap,
  lend,
  assertCondition,
  dynamic,
  erc20Utils,
} from '0xtrails'

const morphoMarketId = 'polygon-usdt-bbqusdt0-0xb7c9988d3922f25a336a469f3bb26ca61fe79e24-4626-vault'
const aaveMarketId   = 'polygon-usdc-aave-v3-lending'

export function ComposedEarnButton({ recipient }: { recipient: `0x${string}` }) {
  const { sendTransaction, isPending, error } = useTrailsSendTransaction({
    actions: [
      // 1. Deposit 0.1 USDT into a Morpho vault
      deposit({
        marketId: morphoMarketId,
        amount: '0.1',
      }),

      // 2. Swap all remaining USDT to USDC
      swap({
        tokenIn: 'USDT',
        tokenOut: 'USDC',
        amountIn: dynamic(), // spend whatever USDT is left
        fee: '0.3',
      }),

      // 3. Guard: revert the whole batch if the swap returned less than 0.08 USDC
      assertCondition({
        erc20Balance: { token: 'USDC', gte: '0.08' },
      }),

      // 4. Lend all resulting USDC into an Aave market
      lend({
        marketId: aaveMarketId,
        amount: dynamic(),
      }),
    ],
    onStatusUpdate: (states) => {
      for (const state of states) console.log('Transaction:', state)
    },
  })

  return (
    <button
      disabled={isPending}
      onClick={() =>
        sendTransaction({
          to: recipient,
          tokenAddress: erc20Utils.USDT.addressOn('polygon'),
          tokenAmount: '0.2', // human-readable USDT amount
        })
      }
    >
      {error ? error.message : isPending ? 'Sending...' : 'Execute'}
    </button>
  )
}
```

`dynamic()` on `amountIn` and `amount` means "use whatever the intent wallet holds at that point". A concrete value like `"0.1"` splits off a fixed slice. Actions run sequentially and any failed `assertCondition` reverts the whole batch, so partial state is never left behind.

## Preview a quote before sending

Use `useQuote` when you want to show the user a breakdown before they commit. Pass the same `actions` array alongside `from` and `to` fields:

```tsx theme={null}
import {
  useQuote,
  deposit,
  swap,
  lend,
  assertCondition,
  dynamic,
} from '0xtrails'

const morphoMarketId = 'polygon-usdt-bbqusdt0-0xb7c9988d3922f25a336a469f3bb26ca61fe79e24-4626-vault'
const aaveMarketId   = 'polygon-usdc-aave-v3-lending'

export function ComposedEarnQuoteUI() {
  const { send, isLoadingQuote, quoteError } = useQuote({
    from: { chain: 'arbitrum', token: 'USDC' },
    to:   { chain: 'polygon',  token: 'USDT', amount: '0.2' },
    actions: [
      deposit({ marketId: morphoMarketId, amount: '0.1' }),
      swap({ tokenIn: 'USDT', tokenOut: 'USDC', amountIn: dynamic(), fee: '0.3' }),
      assertCondition({ erc20Balance: { token: 'USDC', gte: '0.08' } }),
      lend({ marketId: aaveMarketId, amount: dynamic() }),
    ],
    onStatusUpdate: (states) => {
      for (const state of states) console.log('Transaction:', state)
    },
  })

  if (isLoadingQuote) return <p>Quoting...</p>
  if (quoteError) return <p>Error: {quoteError.message}</p>

  return (
    <button disabled={!send} onClick={() => send?.()}>
      Execute
    </button>
  )
}
```

## Discover market IDs at runtime

Hard-coding market IDs is fine for known protocols. For a dynamic UI, use `useEarnMarkets` to fetch available markets and grab the `id` from the result:

```tsx theme={null}
import { useEarnMarkets, useQuote, lend } from '0xtrails'

function LendWithMarketPicker() {
  const { data: markets, isLoading } = useEarnMarkets({
    chain: 'base',
    type: 'lending',
    search: 'usdc',
    sortBy: 'rewardRateDesc',
    limit: 5,
  })

  const market = markets?.[0] // top market by yield

  const { send } = useQuote({
    from: { chain: 'arbitrum', token: 'USDC' },
    to:   { chain: 'base',     token: 'USDC', amount: '100' },
    actions: market
      ? [lend({ marketId: market.id, amount: '100' })]
      : [],
  })

  if (isLoading) return <p>Loading markets...</p>

  return (
    <button disabled={!send} onClick={() => send?.()}>
      Lend on {market?.metadata.name ?? '...'}
    </button>
  )
}
```

## Use a protocol not covered by the builders

Use `custom` as an escape hatch for any protocol.

```tsx theme={null}
import { custom, erc20Utils } from '0xtrails'
import { parseUnits } from 'viem'
import { stakingAbi } from './abi'

const STAKING_CONTRACT = '0x...'
const usdc = erc20Utils.USDC.addressOn('base')

const actions = [
  custom({
    ...erc20Utils.approve({
      tokenAddress: usdc,
      spender: STAKING_CONTRACT,
      amount: parseUnits('100', 6),
    }),
  }),
  custom({
      to: STAKING_CONTRACT,
      data: '0x...'
    }),
]
```

## SDK reference

* [Overview](/sdk/composable-actions/overview) - How composable actions fit into the Trails intent model
* [Building Actions](/sdk/composable-actions/building-actions) - Full API for `swap`, `lend`, `deposit`, `assertCondition`, `custom`
* [Dynamic Values](/sdk/composable-actions/dynamic-values) - How `dynamic()` and `self()` resolve at runtime
* [Markets and Providers](/sdk/composable-actions/markets-and-providers) - Discover market IDs with `useEarnMarkets`
* [ERC-20 Helpers](/sdk/composable-actions/erc20-helpers) - Token registry, `buildCall`, `buildApproveAndCall`, slippage helper
