Skip to main content
Composable actions let you specify a sequence of onchain operations at the destination that execute together after funds arrive. A user signs once on their origin chain; the routing layer bridges and swaps the funds, then executes every action in the sequence from a destination intent wallet. This is how you build flows like: “fund from a card on any chain, swap to WSTETH, deposit into a yield vault”, one signature, no intermediate steps for the user.

How it works

Every composable action flow is two phases:
  1. The user signs a single transaction on their origin chain. Funds bridge and swap to an intent wallet on the destination.
  2. The SDK executes the action sequence from that wallet. All actions run top-down in a single atomic batch, if any step fails, the entire batch reverts.

Quickstart: multi-step DeFi in one intent

This example deposits USDT into Morpho, swaps the remainder to USDC, and lends it into Aave, all in one intent.
import { useTrailsSendTransaction, deposit, swap, lend, dynamic } from "0xtrails";

function MultiStepButton({ morphoMarketId, aaveMarketId }) {
  const { sendTransaction } = useTrailsSendTransaction({
    to: {
      chain: "polygon",
      actions: [
        // Deposit USDT into Morpho
        deposit({
          marketId: morphoMarketId,
          token: "USDT",
          amount: "100",
        }),
        // Swap remaining funds to USDC
        swap({
          tokenIn: "USDT",
          tokenOut: "USDC",
          amountIn: dynamic(), // consume whatever the previous step left
        }),
        // Lend the USDC output into Aave
        lend({
          marketId: aaveMarketId,
          token: "USDC",
          amount: dynamic(),
        }),
      ],
    },
  });

  return <button onClick={sendTransaction}>Execute</button>;
}
dynamic() means “use whatever balance the intent wallet holds at execution time.” You don’t need to predict exact amounts after bridging or swapping. See Dynamic values for details.

Track status across hops

The onStatusUpdate callback fires at each step, bridge confirmation, destination execution, and completion. Use it to drive progress UI:
const { sendTransaction } = useTrailsSendTransaction({
  to: {
    chain: "polygon",
    actions: [...],
  },
  onStatusUpdate: (status) => {
    console.log("Step:", status.step, "State:", status.state);
  },
});

Quote-first pattern

Use useQuote if you want to show the user a preview before sending, or if you want to avoid the SDK modal entirely:
import { useQuote, deposit, dynamic } from "0xtrails";

const { quote, loading } = useQuote({
  from: {
    chainId: 1,
    tokenAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT on Ethereum
    amount: "100",
  },
  to: {
    chainId: 137,
    actions: [
      deposit({
        marketId: "morpho-usdt-vault",
        token: "USDT",
        amount: dynamic(),
      }),
    ],
  },
});

if (quote) {
  console.log("Route preview:", quote.intent.depositAddress);
  await quote.send(); // execute after user confirms
}

Discovering markets

The quickstart example above hard-codes market IDs for clarity. In production, use useEarnMarkets to discover available markets for a given chain, token, and provider at runtime:
import { useEarnMarkets, lend, dynamic, useTrailsSendTransaction } from "0xtrails";

function AaveLendButton({ userAddress }) {
  const { markets } = useEarnMarkets({
    chain: "polygon",
    type: "lending",
    provider: "aave",
    sortBy: "apy",
  });

  const topMarket = markets?.[0];

  const { sendTransaction } = useTrailsSendTransaction({
    to: {
      chain: "polygon",
      actions: topMarket
        ? [lend({ marketId: topMarket.id, token: "USDC", amount: dynamic() })]
        : [],
    },
  });

  return <button onClick={sendTransaction} disabled={!topMarket}>Lend</button>;
}
See React hooks for the full useEarnMarkets reference.

Available action builders

BuilderWhat it does
swap()Token exchange via Uniswap V3 or SushiSwap V3
lend()Supply to a lending market (Aave and others)
deposit()Deposit into a vault (ERC-4626, Morpho, Yearn)
assertCondition()Onchain guard, reverts the batch if the condition fails
custom()Arbitrary contract call via ABI encoding
For the full parameter reference, see Building actions.