Skip to main content

Fund with Trails

Traditional funding flows require users to:
  • Find an external bridge or fiat onramp provider supported by the chain / app
  • Swap to the required deposit token
  • Pay multiple gas fees across different networks
  • Wait for bridge confirmations
  • Navigate complex interfaces
Trails seamlessly enables users to deposit funds into your chain, app, or protocol with optimized UX flows and native integrations with multiple liquidity sources to optimize routes for low-slippage. Onramps, transfers from other dapps, wallet balances, and chain abstraction are all built in by default, so you do not need to implement or expose any separate funding path. Users can fund from any token they hold across any supported chain, eliminating the need for manual bridging and swapping - embedded seamlessly in the experience as a single transaction.

Use Cases

Funding flows in Trails are modeled as exact input by default. For example, “I have exactly 10 USDC tokens I want to send out from the origin chain, and I’ll receive 9.99 USDC on the destination.” Funding can done through a variety of methods such as the user’s connected wallet, an existing app they already have funds, or onramping from various fiat options. This is ideal for a variety of use cases and apps such as the following:
  • Transfer funds into a perpetual exchange deposit address.
  • Bootstrap liquidity for lending protocols by transferring funds and executing a vault deposit.
  • Add funds to your balance on an application with crypto support.
  • Swap and deposit funds into staking contracts.
  • Onboard users to new chains seamlessly from any origin chain.
  • Top up a user’s account on a prediction market.

Examples

Depositing USDC into Polygon

This example shows how to use the Trails widget to enable a user to deposit into a chain with any token, in this case USDC on Polygon with a fixed amount:
import { TrailsWidget } from '0xtrails/widget'

export const BaseDeposit = () => {
  return (
    <TrailsWidget
      apiKey="YOUR_API_KEY"
      mode="fund"
      toAddress="0x..." // Recipient Address
      toChainId={137} // Polygon
      toToken="USDC"
      toAmount="100" // 100 USDC
      onCheckoutComplete={({ sessionId }) => {
        console.log('Deposit completed:', sessionId)
      }}
    />
  )
}

Depositing into a lending protocol

For protocols like Aave, Compound, and Fluid that are natively supported, use the lend composable action. This handles approval, the supply call, and dynamic amount resolution without encoding any calldata by hand:
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', // 100 USDC (6 decimals)
        })
      }
    >
      {isPending ? 'Sending...' : 'Supply 100 USDC to Aave'}
    </button>
  )
}
Use useEarnMarkets to discover available market IDs rather than hard-coding them. See Markets and Providers.

Depositing into a vault

For ERC-4626 and vault-style protocols (Morpho, Yearn, SummerFi), use the deposit composable action. The amount resolves at execution time so slippage and bridge output are handled automatically:
import { useTrailsSendTransaction, deposit, erc20Utils } from '0xtrails'

export function YearnDepositButton({ recipient }: { recipient: `0x${string}` }) {
  const { sendTransaction, isPending } = useTrailsSendTransaction({
    actions: [
      deposit({
        marketId: 'katana-usdc-yearn-v3-vault',
        amount: '100',
      }),
    ],
  })

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

Depositing into a custom contract

If your target protocol is not covered by the lend or deposit builders, use TRAILS_ROUTER_PLACEHOLDER_AMOUNT in the widget’s toCalldata. The widget replaces this constant with the actual bridged/swapped output at execution time:
import { TrailsWidget } from '0xtrails/widget'
import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
import { encodeFunctionData } from 'viem'
import { customVaultABI } from './abi.ts'

export const CustomVaultDeposit = () => {
  const VAULT_CONTRACT = "0x..." // your contract address

  const depositCalldata = encodeFunctionData({
    abi: customVaultABI,
    functionName: 'deposit',
    args: [
      TRAILS_ROUTER_PLACEHOLDER_AMOUNT, // replaced with actual output at execution
      "0x...", // receiver address
    ],
  })

  return (
    <TrailsWidget
      apiKey="YOUR_API_KEY"
      mode="fund"
      toAddress={VAULT_CONTRACT}
      toChainId={747474}
      toToken="USDC"
      toCalldata={depositCalldata}
      theme="auto"
      onCheckoutComplete={({ sessionId }) => {
        console.log('Vault deposit completed:', sessionId)
      }}
    />
  )
}

Creating a trustless deposit address via API (quote → commit → execute)

Deposit addresses are common flows where users can simply deposit funds into based on their requested quote and Trails will automatically orchestrate. The funding mode of the widget handles this for you, but in some cases you may wish to use the API to create your own deposit flows. To create a Trails deposit address, use the backend endpoints to create a quote, commit it, and then execute after the user deposits to the intent address returned in the quote. This gives you a trustless, deterministic deposit address and an intent lifecycle you can track server-side.
const quoteResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/QuoteIntent', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Access-Key': 'YOUR_ACCESS_KEY'
  },
  body: JSON.stringify({
    ownerAddress: userWallet,
    originChainId: 137, // User funds on Polygon
    originTokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon
    destinationChainId: 8453, // Settle on Base
    destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
    destinationToAddress: appCreditAddress, // Address your app credits on destination
    destinationTokenAmount: '50000000', // 50 USDC (6 decimals)
    tradeType: 'EXACT_OUTPUT',
  })
})

const { intent } = await quoteResponse.json()
const intentAddress = intent.depositTransaction.toAddress

const commitResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/CommitIntent', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Access-Key': 'YOUR_ACCESS_KEY'
  },
  body: JSON.stringify({ intent })
})

const { intentId } = await commitResponse.json()

// 1) Ask the user to deposit to the intent address returned in the quote
// 2) When the deposit is confirmed, execute with the deposit transaction hash
const executeResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/ExecuteIntent', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Access-Key': 'YOUR_ACCESS_KEY'
  },
  body: JSON.stringify({
    intentId,
    depositTransactionHash: userDepositTxHash
  })
})

const { intentStatus } = await executeResponse.json()
Quotes expire after 10 minutes.

Next Steps

Explore more about configuring and customizing funding flows: