Documentation Index
Fetch the complete documentation index at: https://anypay-docs-sdk-0-15-0-updates.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
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. For the full builder API, see 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:
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):
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:
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:
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:
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.
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