Bridge tokens from any chain and deposit directly into a DeFi vault on Polygon in a single intent, powered by Trails. Trails handles routing, cross-chain settlement, and the vault deposit call atomically. Pair with the Zerion CLI to fund the wallet and verify the resulting vault position.
Visit https://dashboard.trails.build to create an account and generate a key.
# Widget or hooks (React / Next.js) + calldata encoding
npm install 0xtrails viem
# Direct API (Node.js / backend)
npm install @0xtrails/api viem- "Deposit USDC into [vault] on Polygon"
- "Bridge ETH from Ethereum and deposit into Aave on Polygon"
- "Stake into a yield vault on Polygon from any chain"
- "Put my tokens to work on Polygon"
- Any Aave/Morpho lending market or custom ERC-4626 vault on Polygon
Polygon chain ID: 137
Polygon chain name (widget/hooks): "polygon"
Common deposit tokens on Polygon:
| Symbol | Address |
|---|---|
| USDC (native) | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
| USDC.e | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 |
| USDT | 0xc2132D05D31c914a87C6611C10748AEb04B58e8F |
| WETH | 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 |
| WMATIC / POL | 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270 |
Trails handles the bridge + vault deposit; the Zerion CLI handles funding the wallet beforehand and confirming the vault position afterward. Install with npm i -g zerion-cli.
# 1. Fund the wallet (shows EVM + Solana deposit addresses)
zerion wallet fund --wallet agent-bot
# 2. Confirm the source-chain balance is in place before quoting
zerion portfolio --wallet agent-bot
# 3. Run the Trails deposit (Widget / hooks / API — see sections below)
# 4. Verify the vault position appeared on Polygon (DeFi positions)
zerion portfolio --wallet agent-bot
zerion analyze agent-bot --chain polygon --positions defiThe Trails SDK runs against the same EVM address that zerion wallet list reports — pass that address as ownerAddress / userAddress and use the vault contract as destinationToAddress / to.recipient in any of the integration modes below.
Trails deposits are EXACT_INPUT — the user specifies the input amount and the vault receives the computed settled amount. Because the final amount isn't known at calldata encoding time, import and use TRAILS_ROUTER_PLACEHOLDER_AMOUNT from 0xtrails:
import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
// Trails replaces this with the real settled amount at execution time
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, receiverAddress]Do not use this placeholder for functions that read the token balance internally (e.g. deposit(address receiver) without an explicit amount arg).
Import from 0xtrails/widget. Components are self-contained and take apiKey directly.
import { Fund } from '0xtrails/widget'
import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
import { encodeFunctionData } from 'viem'
const erc4626Abi = [
{
name: 'deposit',
type: 'function',
inputs: [
{ name: 'assets', type: 'uint256' },
{ name: 'receiver', type: 'address' },
],
outputs: [{ name: 'shares', type: 'uint256' }],
},
] as const
function VaultDepositWidget({
vaultAddress,
userAddress,
}: {
vaultAddress: `0x${string}`
userAddress: `0x${string}`
}) {
const calldata = encodeFunctionData({
abi: erc4626Abi,
functionName: 'deposit',
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, userAddress],
})
return (
<Fund
apiKey="YOUR_TRAILS_API_KEY"
to={{
recipient: vaultAddress,
currency: "USDC",
chain: "polygon",
calldata,
}}
onFundingSuccess={({ sessionId }) => {
console.log('Deposit complete:', sessionId)
}}
onFundingError={({ error }) => console.error(error)}
/>
)
}For supported protocols (Aave, Morpho, Yearn, Compound), use the Earn component with composable actions — no manual ABI encoding needed:
import { Earn } from '0xtrails/widget'
function AaveDepositWidget() {
return (
<Earn
apiKey="YOUR_TRAILS_API_KEY"
onEarnSuccess={({ sessionId }) => {
console.log('Earned:', sessionId)
}}
/>
)
}For a specific Polygon lending market, add a to config:
<Earn
apiKey="YOUR_TRAILS_API_KEY"
to={{
chain: "polygon",
currency: "USDC",
}}
onEarnSuccess={({ sessionId }) => console.log(sessionId)}
/>Widget props reference:
| Prop | Type | Description |
|---|---|---|
apiKey |
string | Trails API key (required) |
to.recipient |
string | Vault contract address |
to.currency |
string | Token symbol or address |
to.chain |
string | number | Chain name, ID, or viem Chain |
to.calldata |
string | ABI-encoded deposit call (use TRAILS_ROUTER_PLACEHOLDER_AMOUNT) |
to.amount |
string | Fixed deposit amount (optional) |
paymentMethod |
string | "CONNECTED_WALLET" (default), "CRYPTO_TRANSFER", "CREDIT_DEBIT_CARD", "EXCHANGE" |
Import hooks from 0xtrails. Hooks require TrailsProvider context.
import { TrailsProvider } from '0xtrails'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<TrailsProvider trailsApiKey="YOUR_TRAILS_API_KEY">
{children}
</TrailsProvider>
)
}For supported protocols, use lend() from 0xtrails — Trails handles the contract calls internally:
import { useTrailsSendTransaction, lend, useEarnMarkets } from '0xtrails'
function AaveDepositButton({ amount }: { amount: string }) {
const { data: markets } = useEarnMarkets({ chain: 'polygon', provider: 'aave' })
const { sendTransaction, isPending } = useTrailsSendTransaction({
actions: [
lend({
marketId: 'polygon-usdc-aave-v3-lending', // use useEarnMarkets to discover IDs
amount,
}),
],
})
return (
<button onClick={() => sendTransaction()} disabled={isPending}>
{isPending ? 'Depositing...' : 'Deposit to Aave on Polygon'}
</button>
)
}Use useEarnMarkets to discover available marketId values at runtime rather than hardcoding them:
import { useEarnMarkets } from '0xtrails'
function MarketPicker() {
const { data: markets } = useEarnMarkets({
chain: 'polygon',
type: 'lending', // optional: 'lending' | 'vault'
provider: 'aave', // optional: filter by protocol
sortBy: 'apy', // optional
})
return (
<ul>
{markets?.map(m => (
<li key={m.marketId}>{m.name} — {m.apy}% APY</li>
))}
</ul>
)
}For protocols not natively supported by Trails, encode calldata manually:
import { useTrailsSendTransaction, TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
import { encodeFunctionData } from 'viem'
const erc4626Abi = [
{
name: 'deposit',
type: 'function',
inputs: [
{ name: 'assets', type: 'uint256' },
{ name: 'receiver', type: 'address' },
],
outputs: [{ name: 'shares', type: 'uint256' }],
},
] as const
function useCustomVaultDeposit(vaultAddress: `0x${string}`, userAddress: `0x${string}`) {
const { sendTransaction, isPending } = useTrailsSendTransaction()
const deposit = () => {
const calldata = encodeFunctionData({
abi: erc4626Abi,
functionName: 'deposit',
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, userAddress],
})
sendTransaction({
to: {
recipient: vaultAddress,
currency: "USDC",
chain: "polygon",
calldata,
},
})
}
return { deposit, isPending }
}Full control over quote → commit → execute → wait. Use for server-side automation or non-React environments.
import { TrailsApi, TradeType } from '@0xtrails/api'
import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
import { encodeFunctionData } from 'viem'
const trailsApi = new TrailsApi('YOUR_TRAILS_API_KEY')
const erc4626Abi = [
{
name: 'deposit',
type: 'function',
inputs: [
{ name: 'assets', type: 'uint256' },
{ name: 'receiver', type: 'address' },
],
outputs: [{ name: 'shares', type: 'uint256' }],
},
] as const
async function depositToPolygonVault(params: {
userAddress: string
vaultAddress: string
depositTokenAddress: string // token the vault accepts on Polygon
originChainId: number
originTokenAddress: string
originTokenAmount: bigint // in source token's smallest unit
}) {
const { userAddress, vaultAddress, depositTokenAddress, originChainId, originTokenAddress, originTokenAmount } = params
// Encode vault deposit call — Trails replaces TRAILS_ROUTER_PLACEHOLDER_AMOUNT at execution
const destinationCallData = encodeFunctionData({
abi: erc4626Abi,
functionName: 'deposit',
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, userAddress as `0x${string}`],
})
// 1. Quote — includes bridge + swap + vault deposit in one route
const { intent, gasFeeOptions } = await trailsApi.quoteIntent({
ownerAddress: userAddress,
originChainId,
originTokenAddress,
originTokenAmount,
destinationChainId: 137, // Polygon
destinationTokenAddress: depositTokenAddress,
destinationToAddress: vaultAddress,
destinationCallData, // capital D
tradeType: TradeType.EXACT_INPUT,
options: {
slippageTolerance: 0.005,
},
})
// 2. Commit — pass the full intent object; must execute within 10 minutes
const { intentId } = await trailsApi.commitIntent({ intent })
// 3. Execute — user signs the intent (gasless path)
await trailsApi.executeIntent({
intentId,
depositSignature: {
intentSignature: await signIntent(intent, walletClient), // EIP-712 sign
selectedGasFeeOption: gasFeeOptions.feeOptions[0],
userNonce: 1,
deadline: Math.floor(Date.now() / 1000) + 3600,
},
})
// Alternative: user submits deposit transaction manually
// await trailsApi.executeIntent({ intentId, depositTransactionHash: '0x...' })
// 4. Poll until bridge + vault deposit settles (can take 1-5 min)
let done = false
let intentReceipt
while (!done) {
;({ intentReceipt, done } = await trailsApi.waitIntentReceipt({ intentId }))
}
if (intentReceipt.status === 'SUCCEEDED') {
console.log('Vault deposit complete:', intentReceipt.destinationTransaction?.txnHash)
} else {
throw new Error(`Deposit failed: ${intentReceipt.originTransaction?.statusReason}`)
}
return intentReceipt
}Note on signIntent: This is your wallet's EIP-712 signing function applied to the intent data. With viem, use signTypedData on the intent's structured data. Refer to the Trails execution docs for the exact typed data schema.
import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from '0xtrails'
import { encodeFunctionData } from 'viem'
const calldata = encodeFunctionData({
abi: [{
name: 'deposit',
type: 'function',
inputs: [
{ name: 'assets', type: 'uint256' },
{ name: 'receiver', type: 'address' },
],
outputs: [{ name: 'shares', type: 'uint256' }],
}] as const,
functionName: 'deposit',
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT, receiverAddress],
})const calldata = encodeFunctionData({
abi: [{
name: 'deposit',
type: 'function',
inputs: [{ name: 'amount', type: 'uint256' }],
outputs: [],
}] as const,
functionName: 'deposit',
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT],
})const calldata = encodeFunctionData({
abi: [{
name: 'stake',
type: 'function',
inputs: [{ name: 'amount', type: 'uint256' }],
outputs: [],
}] as const,
functionName: 'stake',
args: [TRAILS_ROUTER_PLACEHOLDER_AMOUNT],
})| Parameter | Type | Description |
|---|---|---|
ownerAddress |
string | User's wallet address |
originChainId |
number | Source chain ID |
originTokenAddress |
string | Source token address |
originTokenAmount |
bigint | Amount in smallest unit |
destinationChainId |
number | 137 for Polygon |
destinationTokenAddress |
string | Token the vault accepts |
destinationToAddress |
string | Vault contract address |
destinationCallData |
string | ABI-encoded vault call (capital D) |
tradeType |
TradeType | TradeType.EXACT_INPUT for all deposit flows |
- Import
TRAILS_ROUTER_PLACEHOLDER_AMOUNT— do not use a rawBigInt('0xff...')constant. - Verify the vault contract — confirm
destinationToAddress/to.recipientis a trusted contract. - Check token decimals — USDC uses 6 decimals; WETH and POL use 18.
- Test calldata encoding — decode with
decodeFunctionDatafromviembefore using in production. - Confirm vault accepts bridged token — some vaults accept native USDC (
0x3c49...) only, not USDC.e (0x2791...). - Committed intents expire in 10 minutes — quote and execute promptly.
| Code | Cause | Fix |
|---|---|---|
missing_api_key |
API key not set | Check TRAILS_API_KEY or apiKey prop |
quote_failed |
No route to vault token | Confirm vault's deposit token is Trails-supported |
quote_expired |
>5 min since quote | Re-quote and commit immediately |
intent_expired |
>10 min since commit | Re-quote, re-commit, then execute |
tx_reverted |
Vault deposit call reverted | Verify ABI, token address, and placeholder usage |
slippage_exceeded |
Price moved beyond tolerance | Increase slippageTolerance or retry |
unsupported_chain |
Polygon not available | Call getChains() to verify |