Skip to main content

Overview

Earn mode enables users to deposit into DeFi protocols for yield generation and staking. It supports both pre-integrated protocols (Aave, Morpho) and custom contract integrations with arbitrary calldata.
Earn mode includes both deposit and withdrawal tabs. To deep-link users directly into withdrawals, use Withdraw mode.

Quick start

import { Earn } from '0xtrails/widget'

// Open protocol selection — user picks everything
<Earn apiKey="YOUR_API_KEY" />
Pre-configure the destination protocol:
<Earn
  apiKey="YOUR_API_KEY"
  to={{
    recipient: "0xYearnVault",
    currency: "USDC",
    chain: "katana",
    calldata: encodedDepositCalldata,
  }}
  onEarnSuccess={({ sessionId }) => console.log("deposited", sessionId)}
/>

Props

Required

PropTypeDescription
apiKeystringTrails API key

Destination (optional)

PropTypeDescription
to.recipientstringProtocol contract address
to.tokenstringToken to deposit — symbol or contract address
to.chainChainIdentifierProtocol chain — name, ID, or viem Chain
to.defaultTokenstringDefault token — user can change
to.defaultChainChainIdentifierDefault chain — user can change
to.amountstring | numberFixed deposit amount
to.calldatastringABI-encoded function call to execute after deposit

Source (optional)

The from shape is a discriminated union on paymentMethod. Crypto source (paymentMethod is "CONNECTED_WALLET", "EXCHANGE", "CRYPTO_TRANSFER", or omitted):
PropTypeDescription
from.tokenstringSource ERC-20 token — symbol or contract address
from.chainChainIdentifierSource chain
from.amountstring | numberSource amount
from.walletAddressstringSource wallet (defaults to connected wallet)
from.exchangestringMesh exchange key (e.g. "coinbase"). EXCHANGE arm only.
Fiat source (paymentMethod is "CREDIT_DEBIT_CARD"):
PropTypeDescription
from.currencystringISO 4217 fiat code, e.g. "USD", "EUR", "GBP"
from.amountstring | numberFiat amount to charge

Payment method (optional)

paymentMethod controls how the user sources funds:
ValueMethod
"CONNECTED_WALLET"Connected wallet (default)
"CRYPTO_TRANSFER"QR code / address deposit
"CREDIT_DEBIT_CARD"Fiat on-ramp
"EXCHANGE"CEX transfer

Lifecycle callbacks

CallbackSignatureWhen it fires
onEarnStart({ sessionId }) => voidUser begins the deposit flow
onEarnSuccess({ sessionId }) => voidDeposit completes successfully
onEarnError({ sessionId, error }) => voidDeposit encounters an error

Examples

Protocol selection — user picks everything

import { Earn } from '0xtrails/widget'

<Earn
  apiKey="YOUR_API_KEY"
  onEarnSuccess={({ sessionId }) => console.log("deposited", sessionId)}
/>

Fixed amount protocol deposit

Deposit 1 USDC into Aave V3 on Arbitrum:
import { Earn } from '0xtrails/widget'
import { encodeFunctionData } from 'viem'

const AAVE_POOL = '0x794a61358D6845594F94dc1DB02A252b5b4814aD'
const USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'

const supplyCalldata = encodeFunctionData({
  abi: [{
    name: 'supply',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'asset', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'onBehalfOf', type: 'address' },
      { name: 'referralCode', type: 'uint16' },
    ],
    outputs: [],
  }],
  functionName: 'supply',
  args: [USDC_ADDRESS, 1000000n, '0xUserAddress', 0],
})

<Earn
  apiKey="YOUR_API_KEY"
  to={{
    recipient: AAVE_POOL,
    currency: "USDC",
    chain: "arbitrum",
    amount: "1",
    calldata: supplyCalldata,
  }}
>
  <button>Deposit 1 USDC to Aave</button>
</Earn>

ERC-4626 vault with dynamic amount

Use TRAILS_ROUTER_PLACEHOLDER_AMOUNT when the deposit amount is a parameter and the user selects it:
import { Earn } from '0xtrails/widget'
import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
import { encodeFunctionData } from 'viem'

const depositCalldata = encodeFunctionData({
  abi: [{
    name: 'deposit',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'assets', type: 'uint256' },
      { name: 'receiver', type: 'address' },
    ],
    outputs: [{ name: 'shares', type: 'uint256' }],
  }],
  functionName: 'deposit',
  args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, '0xUserAddress'],
})

<Earn
  apiKey="YOUR_API_KEY"
  to={{
    recipient: "0xVaultContract",
    currency: "USDC",
    chain: "base",
    calldata: depositCalldata,
  }}
>
  <button>Deposit to Vault</button>
</Earn>
Use TRAILS_ROUTER_PLACEHOLDER_AMOUNT when:
  • Deposit amount is a function parameter
  • User selects the deposit amount
  • Amount needs to reflect post-swap/bridge output
Do not use it for functions that read balance internally (e.g. depositAll()).

Composable actions (pre-integrated protocols)

For Aave, Compound, Morpho, Yearn, and other natively supported protocols, use composable actions instead of manual calldata encoding:
import { useTrailsSendTransaction, lend, erc20Utils } from '0xtrails'

export function AaveLendButton({ 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: '100000000',
        })
      }
    >
      {isPending ? 'Sending...' : 'Supply 100 USDC to Aave'}
    </button>
  )
}
Use useEarnMarkets to discover available market IDs. See Markets and Providers.

Calldata reference

When to use TRAILS_ROUTER_PLACEHOLDER_AMOUNT

// Correct — dynamic amount in calldata
encodeFunctionData({
  functionName: 'deposit',
  args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, userAddress],
})

// Correct — function reads balance directly, no placeholder needed
encodeFunctionData({
  functionName: 'depositAll',
  args: [userAddress],
})
TRAILS_ROUTER_PLACEHOLDER_AMOUNT is uint256.max internally. Trails replaces it with the actual output amount at execution time.

See also