# AbortIntent Source: https://docs.trails.build/api-reference/endpoints/abort-intent trails-api.gen.json post /rpc/Trails/AbortIntent ## Overview The `AbortIntent` endpoint cancels an in-flight intent after you have submitted the abort transaction on-chain. Once the abort transaction is confirmed, call this endpoint to notify Trails so the intent status is updated to `ABORTED` and any recoverable funds are tracked. ## Use Cases * Cancel a pending intent that has not yet been processed * Recover stuck or expired intents * Clean up failed cross-chain transactions ## Request Parameters ### Required Fields * **intentId** (string): The unique ID of the intent to abort * **chainId** (number): The chain ID where the abort transaction was submitted * **abortTransactionHash** (string): The transaction hash of the on-chain abort transaction ## Response * **intentId** (string): The ID of the aborted intent * **status** (`IntentStatus`): Updated status of the intent (typically `ABORTED`) ## Examples ### Abort a Pending Intent ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/AbortIntent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_abc123', chainId: 1, abortTransactionHash: '0xabc123...' }) }); const { intentId, status } = await response.json(); console.log(`Intent ${intentId} is now ${status}`); ``` ### With the Trails SDK ```typescript theme={null} import { TrailsClient } from '0xtrails' const trails = new TrailsClient({ accessKey: 'YOUR_ACCESS_KEY' }) const result = await trails.abortIntent({ intentId: 'intent_abc123', chainId: 1, abortTransactionHash: '0xabc123...' }) console.log('Abort result:', result.status) ``` You must submit the abort transaction on-chain first and wait for it to be confirmed before calling this endpoint. The `abortTransactionHash` must correspond to a valid on-chain transaction. ## Next Steps Query intent history including aborted intents Check current intent status # BuildIntentRecoveryTransaction Source: https://docs.trails.build/api-reference/endpoints/build-intent-recovery-transaction trails-api.gen.json post /rpc/Trails/BuildIntentRecoveryTransaction ## Overview The `BuildIntentRecoveryTransaction` endpoint verifies a signed recovery payload (produced via [`PrepareIntentRecovery`](/api-reference/endpoints/prepare-intent-recovery)) and returns executable transaction data the client can broadcast. The transaction sweeps recoverable balances from a stranded intent wallet to the configured refund address. ## Request Parameters ### Required Fields * **intentId** (string): The unique ID of the intent being recovered. * **payload** (any): The payload returned by `PrepareIntentRecovery`. * **signature** (string): User signature over the typed data from `PrepareIntentRecovery`. * **intentAddress** (string): The intent wallet to recover from (matches `PrepareIntentRecovery` response). ### Optional Fields * **refundToAddress** (string): Defaults to the intent owner address when omitted. ## Response * **to** (string): Transaction `to` address. * **data** (string): Transaction calldata. * **value** (string): Transaction value (bigint). * **chainId** (number): Chain to broadcast on. * **intentAddress** (string) * **requiresDeploy** (boolean): `true` if the intent wallet must be counterfactually deployed as part of this transaction. * **payloadHash** (string, optional) ## Example ```typescript theme={null} // After PrepareIntentRecovery and user signing const response = await fetch('https://trails-api.sequence.app/rpc/Trails/BuildIntentRecoveryTransaction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_abc123', payload, // from PrepareIntentRecovery signature, // user signature over typedData intentAddress // from PrepareIntentRecovery }) }); const tx = await response.json(); const hash = await walletClient.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value), chainId: tx.chainId }); ``` ## Next Steps Generate the payload to sign Check intent state after recovery # BuildOIFRefundTransaction Source: https://docs.trails.build/api-reference/endpoints/build-oif-refund-transaction trails-api.gen.json post /rpc/Trails/BuildOIFRefundTransaction ## Overview The `BuildOIFRefundTransaction` endpoint returns the direct on-chain transaction needed to refund an expired or unfilled OIF (Open Intents Framework) order through its input settler. After the order's `orderExpiresAt` deadline passes without a solver fill, any wallet can broadcast this transaction to return funds to the user. ## Use Cases * Refund an OIF order whose solver did not fill before expiry * Self-serve recovery from a stranded OIF deposit ## Request Parameters ### Required Fields * **intentId** (string): The intent whose OIF order should be refunded. ## Response * **intentId** (string) * **orderId** (string, optional): The on-chain OIF order ID, when known. * **to** (string): Transaction `to` address — the input settler contract. * **data** (string): Transaction calldata. * **value** (string): Transaction value (bigint). * **chainId** (number): Chain to broadcast on. * **orderExpiresAt** (string, optional): The expiry timestamp; the refund cannot be submitted until after this time. * **ready** (boolean): `true` when the order is in a state where the refund can be submitted now. * **status** (string): Current order state. ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/BuildOIFRefundTransaction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_abc123' }) }); const tx = await response.json(); if (!tx.ready) { console.log('OIF order is not yet refundable. Status:', tx.status, 'expires:', tx.orderExpiresAt); return; } const hash = await walletClient.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value), chainId: tx.chainId }); ``` OIF refunds are only available for intents that used the `OIF` route provider. See [Route Providers — OIF](/sdk/quote-providers#oif-open-intents-framework) for routing details. ## Next Steps Inspect the intent's state and OIF order details Notify Trails after broadcasting the refund # CommitIntent Source: https://docs.trails.build/api-reference/endpoints/commit-intent trails-api.gen.json post /rpc/Trails/CommitIntent **Deprecated for new integrations.** `CommitIntent` remains behaviorally stable for existing callers, but new integrations should call [`ExecuteIntent`](/api-reference/endpoints/execute-intent) directly on a `QUOTED` intent. For a Solana or Tron origin, arm fill monitoring with [`ExecuteIntentEdge`](/api-reference/endpoints/execute-intent-edge) instead. ## Overview The `CommitIntent` endpoint validates a quoted intent, atomically arms origin edge quotes, and transitions the intent from `QUOTED` to `COMMITTED`. New clients skip this step — `ExecuteIntent` accepts `QUOTED` intents directly and folds in the same validation. ## Use Cases * Legacy integrations that still separate the commit step from execution * Preserving compatibility with older SDKs that call `CommitIntent` before `ExecuteIntent` ## Request Parameters ### Required Fields * **intent** (Intent): The complete intent object returned from `QuoteIntent` ## Response The response includes: * **intentId** (string): Unique identifier for the committed intent ## Intent Lifecycle Legacy flow (existing callers): ``` QuoteIntent → CommitIntent → ExecuteIntent → Receipt ``` Current flow (new integrations): ``` QuoteIntent → ExecuteIntent → Receipt ``` For Solana/Tron origins, replace `ExecuteIntent` with `ExecuteIntentEdge` to arm fill monitoring before the user pays on the edge chain. ## Example ```typescript theme={null} // First, get a quote 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(quoteRequest) }); const { intent, gasFeeOptions } = await quoteResponse.json(); // Then commit the intent 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(); console.log('Intent committed:', intentId); ``` ## Important Notes Committed intents share the quote's expiry — call `ExecuteIntent` within **15 minutes** of the original `QuoteIntent` call. `CommitIntent` does not extend the window; if the quote expires, request a fresh one. The intent object from `QuoteIntent` must be forwarded unmodified — the server rejects mutated intents. ## Next Steps After committing an intent: 1. Prepare the transaction signature using the provided intent details for the user to deposit into the intent address. 2. Execute the transaction with [`ExecuteIntent`](/api-reference/endpoints/execute-intent). 3. Monitor the transaction status with [`WaitIntentReceipt`](/api-reference/endpoints/wait-intent-receipt). # ExecuteIntent Source: https://docs.trails.build/api-reference/endpoints/execute-intent trails-api.gen.json post /rpc/Trails/ExecuteIntent ## Overview The `ExecuteIntent` endpoint initiates the execution of a quoted intent. It accepts intents directly out of `QuoteIntent` — no separate `CommitIntent` step is required for new integrations. Existing callers that still commit before executing continue to work unchanged. The call triggers the cross-chain transaction flow: deposit confirmation, swaps, bridges, and destination calls. For Solana or Tron origins, call [`ExecuteIntentEdge`](/api-reference/endpoints/execute-intent-edge) instead. `ExecuteIntent` covers the EVM-to-EVM path; the edge equivalent arms a Relay fill monitor and doesn't take a deposit hash — the server discovers it on-chain. ## Use Cases * Execute a quoted cross-chain transaction * Initiate gasless transactions with deposit signatures * Trigger intent execution after deposit confirmation * Start the cross-chain bridging process ## Request Parameters ### Required Fields * **intentId** (string): The intent identifier — read from `QuoteIntentResponse.intent.intentId` (or the `intentId` returned by `CommitIntent` for legacy flows) ### Optional Fields * **depositTransactionHash** (string): Hash of the deposit transaction if already executed * **depositSignature** (DepositSignature): Signature for gasless execution * **intentSignature** (string): EIP-712 signature of the intent * **permitSignature** (string): ERC-2612 permit signature (if applicable) * **permitDeadline** (number): Permit expiration timestamp * **permitAmount** (number): Amount authorized by permit * **selectedGasFeeOption** (FeeOption): Selected gas fee payment option * **userNonce** (number): User's nonce for replay protection * **deadline** (number): Signature expiration timestamp ## Response The response includes: * **intentId** (string): The intent identifier * **intentStatus** (IntentStatus): Current status of the intent * `QUOTED`: Initial quote generated * `COMMITTED`: Intent committed and ready * `EXECUTING`: Currently executing * `FAILED`: Execution failed * `SUCCEEDED`: Successfully completed ## Execution Options ### Mode 1: With Deposit Transaction Hash First, deposit tokens to the intent address, then call ExecuteIntent with the transaction hash: ```typescript theme={null} import { encodeFunctionData } from 'viem'; // Step 1: Send tokens to the intent address const depositTx = await walletClient.sendTransaction({ to: intent.depositTransaction.tokenAddress, // Intent address from quote value: 0n, // 0 for ERC20 tokens, or native ETH amount data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [ intent.depositTransaction.toAddress as `0x${string}`, BigInt(intent.depositTransaction.amount) ] }) }); // Step 2: Wait for confirmation const receipt = await publicClient.waitForTransactionReceipt({ hash: depositTx }); // Step 3: Execute the intent 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: 'intent_123abc', depositTransactionHash: depositTx }) }); const { intentStatus } = await executeResponse.json(); console.log('Execution started:', intentStatus); ``` ### Mode 2: With Deposit Signature (Gasless) Trails also supports paying end to end in non-native gas tokens that are permit compatible: ```typescript theme={null} // Prepare signature const signature = await signIntent(intent, walletClient); 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: 'intent_123abc', depositSignature: { intentSignature: signature, selectedGasFeeOption: gasFeeOptions.feeOptions[0], userNonce: 1, deadline: Math.floor(Date.now() / 1000) + 3600 } }) }); const { intentStatus } = await executeResponse.json(); console.log('Execution started:', intentStatus); ``` For complete details on preparing the deposit signature with permit and intent signatures, see the [Alternative Fee Token Flow](/api-reference/introduction#alternative-fee-token-flow) in the Getting Started guide. ## Transaction Flow Once executed, the intent goes through several stages: 1. **Deposit**: Tokens are deposited to the origin intent address 2. **Relaying**: Transaction is relayed to destination chain (if cross-chain) 3. **Execution**: Destination calls are executed if calldata is passed 4. **Completion**: Final status is recorded ## Example: Full Flow ```typescript theme={null} import { TradeType, TrailsApi } from '@0xtrails/api' const trailsApi = new TrailsApi('YOUR_API_KEY') const quoteRequest = { ownerAddress: "0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1", originChainId: 1, // Ethereum originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC originTokenAmount: 100000000n, // 100 USDC (6 decimals) destinationChainId: 8453, // Base destinationTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base destinationToAddress: "0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1", tradeType: TradeType.EXACT_INPUT, options: { slippageTolerance: 0.005, // 0.5% quoteProvider: "RELAY" } }; // Step 1: Quote const quoteResponse = await trailsApi.quoteIntent(quoteRequest); const { intent, gasFeeOptions } = quoteResponse; // Step 2: Execute directly from QUOTED (no separate commit step) const { intentStatus } = await trailsApi.executeIntent({ intentId: intent.intentId, depositSignature: { intentSignature: await signIntent(intent), selectedGasFeeOption: gasFeeOptions.feeOptions[0], userNonce: 1, deadline: Math.floor(Date.now() / 1000) + 3600 } }); console.log('Status:', intentStatus); // EXECUTING // Step 3: Monitor const receipt = await trailsApi.waitIntentReceipt({ intentId: intent.intentId }); console.log('Receipt:', receipt); ``` ## Timing Requirements Call `ExecuteIntent` within the quote's expiration window (**15 minutes** from `QuoteIntent`). If the quote expires before execution, request a new quote and execute again. Legacy callers that committed first still share this same quote-expiry window — `CommitIntent` does not extend it. ## Error Handling If execution fails, the intent status will be set to `FAILED`. Check the `statusReason` field in the receipt for details. ## Next Steps After executing an intent: 1. Use `GetIntentReceipt` to poll for transaction status 2. Use `WaitIntentReceipt` for streaming updates 3. Check transaction hashes on block explorers 4. Verify final balances on destination chain # ExecuteIntentEdge Source: https://docs.trails.build/api-reference/endpoints/execute-intent-edge trails-api.gen.json post /rpc/Trails/ExecuteIntentEdge ## Overview `ExecuteIntentEdge` arms an origin-mode edge intent — one whose funding leg comes from a non-EVM **edge rail** (Solana or Tron) — so the edge monitor starts watching for the external bridge fill server-side. Call it on user "proceed" before the user pays on the edge chain; no deposit is attached because the fill hash is discovered server-side once funds arrive. Use `ExecuteIntent` for EVM-only intents. Use `ExecuteIntentEdge` for intents quoted through [`QuoteIntentEdge`](/api-reference/endpoints/quote-intent-edge) with `edge.mode = ORIGIN`. Destination-mode edge intents are armed atomically inside `ExecuteIntent` and do not use this endpoint. ## Request Parameters ### Required Fields * **intentId** (string): The intent identifier returned from `QuoteIntentEdge` (or from `CommitIntent` if you took the two-step path). The edge quote is resolved from the intent server-side, so no `edgeId` is required. ## Response * **intentId** (string): The intent identifier. * **intentStatus** (`IntentStatus`): Current status of the intent. Arming does not itself change the intent's status — the intent stays `QUOTED` until the edge monitor observes the fill and completes execution server-side. Once the fill is observed, the intent transitions to `EXECUTING` and then to `SUCCEEDED` or `FAILED`. ## Behavior * Arming is idempotent — re-arming an already-armed intent succeeds without error. * Arming is pre-funds by definition: the endpoint rejects with `QuoteExpired` if the quote has expired. * No `intent_receipts` or `intent_transactions` rows are created at arm time. The edge monitor writes them as the fill and subsequent hops progress. ## Example ```typescript theme={null} // Step 1: Quote an origin-mode edge intent (e.g. Solana → Base USDC) const quoteResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/QuoteIntentEdge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ intent: { ownerAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', originChainId: 0, destinationChainId: 8453, destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', destinationToAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', destinationTokenAmount: 100000000, }, edge: { rail: 'SOLANA', mode: 'ORIGIN', originChainId: 1399811149, originAddress: 'AbC...', originTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', }, }), }) const { intent } = await quoteResponse.json() // Step 2: Arm the intent so the edge monitor starts watching for the fill const armResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/ExecuteIntentEdge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ intentId: intent.id, }), }) const { intentStatus } = await armResponse.json() // Step 3: Guide the user to pay on the edge chain to `edge.handoff`. // Step 4: Poll GetIntentReceipt / WaitIntentReceipt for completion. ``` ## Timing Requirements Arm the intent within the quote's expiration window (**15 minutes** from `QuoteIntentEdge`). After arming, the user must complete the edge deposit before the intent's overall expiry, otherwise the intent refunds. ## Errors | Error | When | | - | - | | `InvalidArgument` | `intentId` missing, or the intent has no origin edge quote | | `NotFound` | No intent matches `intentId` | | `IntentStatus` | Intent is past its pre-execution states (already `EXECUTING`, `SUCCEEDED`, `FAILED`, etc.) | | `QuoteExpired` | The intent's quote expired before it was armed | | `IntentsDisabled` | Intents are disabled on this deployment | ## Next Steps Poll for cross-chain execution status Long-poll for updates # GetChains Source: https://docs.trails.build/api-reference/endpoints/get-chains trails-api.gen.json post /rpc/Trails/GetChains ## Overview The `GetChains` endpoint retrieves a list of all blockchain networks supported by Trails. This is useful for building chain selection UIs, validating user inputs, and discovering which networks are available for cross-chain transactions. ## Use Cases * Build chain selection dropdowns in your UI * Validate that user-selected chains are supported * Display chain metadata (name, logo, native token) * Filter chains by bridging support * Identify testnet vs mainnet networks * Get block explorer URLs for transaction links ## Request Parameters ### Optional Fields * **routeProvider** (string): Filter chains by a specific route provider (e.g., "CCTP", "RELAY") ## Response The response includes: * **chains** (ChainInfo\[]): Array of supported chain information ### ChainInfo Object Structure Each chain object contains: * **id** (number): The chain ID * **name** (string): Human-readable chain name * **tokenName** (string): Native token name (e.g., "Ether") * **tokenSymbol** (string): Native token symbol (e.g., "ETH") * **tokenDecimals** (number): Native token decimals * **isTestnet** (boolean): Whether this is a testnet * **supportsBridging** (boolean): Whether bridging is supported on this chain * **logoUri** (string, optional): URL to chain logo * **blockExplorerUrl** (string, optional): Block explorer base URL ## Examples ### Get All Supported Chains ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetChains', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }); const { chains } = await response.json(); console.log('Supported chains:', chains.length); chains.forEach(chain => { console.log(`${chain.name} (${chain.id}): ${chain.tokenSymbol}`); }); ``` ### Filter by Route Provider ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetChains', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ routeProvider: 'CCTP' // Get chains supported by Circle's CCTP }) }); const { chains } = await response.json(); console.log('CCTP-supported chains:', chains.map(c => c.name)); ``` ### Build Chain Selector UI ```tsx theme={null} import { useEffect, useState } from 'react'; interface ChainInfo { id: number; name: string; tokenSymbol: string; logoUri?: string; isTestnet: boolean; supportsBridging: boolean; } export const ChainSelector = ({ onSelect, excludeTestnets = true }: { onSelect: (chainId: number) => void; excludeTestnets?: boolean; }) => { const [chains, setChains] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch('https://trails-api.sequence.app/rpc/Trails/GetChains', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }) .then(res => res.json()) .then(({ chains }) => { const filtered = excludeTestnets ? chains.filter((c: ChainInfo) => !c.isTestnet) : chains; setChains(filtered); setLoading(false); }); }, [excludeTestnets]); if (loading) return
Loading chains...
; return ( ); }; ``` ### Get Chains with Bridging Support ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetChains', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }); const { chains } = await response.json(); // Filter to only chains that support bridging const bridgableChains = chains.filter(chain => chain.supportsBridging); console.log('Chains with bridging support:'); bridgableChains.forEach(chain => { console.log(`- ${chain.name} (${chain.id})`); }); ``` ## Common Chain IDs | Chain | Chain ID | Native Token | | - | - | - | | Ethereum | 1 | ETH | | Polygon | 137 | MATIC | | Arbitrum One | 42161 | ETH | | Optimism | 10 | ETH | | Base | 8453 | ETH | | Avalanche | 43114 | AVAX | | BNB Chain | 56 | BNB | Chain support is continuously expanding. Use this endpoint to get the current list rather than hardcoding chain IDs. ## Next Steps Get available tokens for selected chains Request a quote for cross-chain transactions # GetDefaultIntentProtocol Source: https://docs.trails.build/api-reference/endpoints/get-default-intent-protocol trails-api.gen.json post /rpc/Trails/GetDefaultIntentProtocol ## Overview `GetDefaultIntentProtocol` returns the intent-protocol version the Trails API defaults to when no `intentProtocol` is specified on a quote or commit. Clients that pin to a specific version should ignore this; clients that want to track the latest stable protocol without shipping a new build can call this at startup. See [`GetSupportedIntentProtocols`](/api-reference/endpoints/get-supported-intent-protocols) for the full list of versions the API accepts, and [`GetProtocolContracts`](/api-reference/endpoints/get-protocol-contracts) to resolve the deployed contract addresses for a version. ## Request `GetDefaultIntentProtocol` takes no parameters. ## Response * **version** (`IntentProtocolVersion`) — the current default. Values: `v1`, `v1_5`. ## Example ```typescript theme={null} const response = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetDefaultIntentProtocol', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({}), }, ) const { version } = await response.json() console.log('Default intent protocol:', version) ``` ## Next Steps List every supported intent-protocol version Look up the contract addresses for a protocol version # GetEarnPools (deprecated) Source: https://docs.trails.build/api-reference/endpoints/get-earn-pools trails-api.gen.json post /rpc/Trails/GetEarnPools `GetEarnPools` is deprecated. Use [YieldGetMarkets](/api-reference/endpoints/yield-get-markets) instead — it provides richer market data, filtering, and pagination. ## Overview The `GetEarnPools` endpoint returns aggregated yield-bearing pool information from supported DeFi protocols such as Aave and Morpho. Use this to display earning opportunities to users and build Earn mode integrations. ## Use Cases * Display yield pools with APY and TVL in your UI * Filter pools by chain or protocol * Power the Trails Earn mode with live pool data * Show users the best yield opportunities across chains ## Request Parameters All fields are optional. * **chainIds** (number\[]): Filter pools to specific chain IDs * **protocols** (string\[]): Filter by protocol name (e.g. `"aave"`, `"morpho"`) * **minTvl** (number): Minimum total value locked (USD) filter * **maxApy** (number): Maximum APY filter ## Response * **pools** (`EarnPool[]`): Array of yield pool objects * **timestamp** (string): ISO timestamp of when this data was fetched * **cached** (boolean): Whether the response is from cache ### EarnPool Object Structure Each pool includes: * **id** (string): Unique pool identifier * **name** (string): Human-readable pool name * **protocol** (string): Protocol name (e.g. `"aave"`, `"morpho"`) * **chainId** (number): Chain the pool is deployed on * **apy** (number): Current annual percentage yield * **tvl** (number): Total value locked in USD * **token** (`PoolTokenInfo`): Deposit token details (symbol, name, address, decimals) * **depositAddress** (string): Contract address to deposit to * **isActive** (boolean): Whether the pool is currently accepting deposits * **poolUrl** (string, optional): URL to the pool on the protocol's UI * **protocolUrl** (string, optional): URL to the protocol's website * **wrappedTokenGatewayAddress** (string, optional): Gateway address for native token deposits ## Examples ### Get All Earn Pools ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetEarnPools', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }); const { pools, timestamp, cached } = await response.json(); console.log(`Fetched ${pools.length} pools (cached: ${cached})`); pools.forEach(pool => { console.log(`${pool.name} on chain ${pool.chainId}: ${pool.apy.toFixed(2)}% APY, $${pool.tvl.toLocaleString()} TVL`); }); ``` ### Filter by Chain and Protocol ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetEarnPools', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: [8453], // Base only protocols: ['aave'], minTvl: 1000000 // At least $1M TVL }) }); const { pools } = await response.json(); ``` ### Build an Earn Pool List UI ```tsx theme={null} import { useEffect, useState } from 'react'; interface EarnPool { id: string; name: string; protocol: string; chainId: number; apy: number; tvl: number; token: { symbol: string; address: string }; depositAddress: string; isActive: boolean; } export const EarnPoolList = ({ chainId }: { chainId?: number }) => { const [pools, setPools] = useState([]); useEffect(() => { fetch('https://trails-api.sequence.app/rpc/Trails/GetEarnPools', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: chainId ? [chainId] : undefined }) }) .then(res => res.json()) .then(({ pools }) => setPools(pools.filter((p: EarnPool) => p.isActive))); }, [chainId]); return ( ); }; ``` ## Next Steps Use earn pools in the Trails widget Discover supported chains for earn pools # GetEdgeStatus Source: https://docs.trails.build/api-reference/endpoints/get-edge-status trails-api.gen.json post /rpc/Trails/GetEdgeStatus ## Overview The `GetEdgeStatus` endpoint returns the current status of an edge rail leg created by [`QuoteIntentEdge`](/api-reference/endpoints/quote-intent-edge). Use it to track the external (Solana / Tron) bridge hop's progress alongside the inner intent's `WaitIntentReceipt`/`GetIntentReceipt` stream. ## Request Parameters ### Required Fields * **edgeId** (string): The Trails edge quote ID returned by `QuoteIntentEdge`. The server resolves the underlying `relayRequestId` and origin chain from this ID, then queries the external provider. ## Response * **status** (string): The edge rail status. Maps to the `EdgeStatus` enum values: `PENDING`, `DEPOSITED`, `FILLED`, `REFUNDED`, `FAILED`. * **initTransactionHash** (string, optional): The transaction hash that initiated the edge leg (deposit on the non-EVM origin chain, or handoff transfer for destination mode). * **initChainId** (number, optional): Chain ID the `initTransactionHash` was submitted on. * **fillTransactionHash** (string, optional): The transaction hash that filled the edge leg. * **fillChainId** (number, optional): Chain ID the `fillTransactionHash` was submitted on. * **refundTransactionHash** (string, optional): The transaction hash that refunded the edge leg when it could not be filled. * **refundChainId** (number, optional): Chain ID the `refundTransactionHash` was submitted on. * **transactionStates** (array, optional): Detailed per-transaction state info. * **failReason** (string, optional): Populated when the edge rail failed. ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetEdgeStatus', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ edgeId: 'edge_abc123' }) }); const status = await response.json(); if (status.status === 'FILLED') { console.log('Edge filled, fill tx:', status.fillTransactionHash); } else if (status.status === 'REFUNDED') { console.log('Edge refunded:', status.refundTransactionHash); } ``` ## Next Steps Create the edge quote Stream the inner intent's receipt updates # GetEdges Source: https://docs.trails.build/api-reference/endpoints/get-edges trails-api.gen.json post /rpc/Trails/GetEdges ## Overview The `GetEdges` endpoint returns the edge rails and modes currently enabled on this Trails deployment. Use it to discover whether Solana or Tron edges are available — and whether they are configured for `ORIGIN`, `DESTINATION`, or both — before calling [`QuoteIntentEdge`](/api-reference/endpoints/quote-intent-edge). ## Request `GetEdges` takes no parameters. ## Response * **edges** (array of `EdgeRailModes`): * **rail** (`EdgeRail`): `SOLANA` or `TRON`. * **modes** (array of `EdgeMode`): One or both of `ORIGIN`, `DESTINATION`. ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetEdges', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }); const { edges } = await response.json(); for (const edge of edges) { console.log(`${edge.rail} supports modes:`, edge.modes); } ``` ## Next Steps Quote a cross-chain intent using an edge rail Track an edge rail's status # GetExactInputRoutes Source: https://docs.trails.build/api-reference/endpoints/get-exact-input-routes trails-api.gen.json post /rpc/Trails/GetExactInputRoutes ## Overview The `GetExactInputRoutes` endpoint retrieves available token routes for exact input swaps. Given a source token, it returns all destination tokens that can be received. This is essential for building UIs where users specify exactly how much they want to spend. ## Use Cases * Build "I want to spend exactly X tokens" interfaces * Find which destination tokens are available from a source * "Swap all" functionality where user empties a token balance * Portfolio rebalancing flows * Cross-chain token migration ## Request Parameters ### Required Fields * **originChainId** (number): The chain ID of the source token * **originTokenAddress** (string): The token address to swap from ### Optional Fields * **destinationChainId** (number): Filter routes to a specific destination chain * **destinationTokenAddress** (string): Check if a specific route exists ## Response The response includes: * **tokens** (TokenInfo\[]): Array of destination tokens that can be received ### TokenInfo Object Structure Each token object contains: * **chainId** (number): Chain where the token exists * **address** (string): Token contract address * **name** (string): Token name * **symbol** (string): Token symbol * **decimals** (number): Token decimals * **supportsBridging** (boolean, optional): Whether the token supports cross-chain bridging * **logoUri** (string, optional): URL to token logo * **featured** (boolean): Whether this is a featured/popular token * **featureIndex** (number): Sort order for featured tokens ## Examples ### Find Destination Tokens for ETH on Ethereum ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactInputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ originChainId: 1, // Ethereum originTokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' // WETH }) }); const { tokens } = await response.json(); console.log('Tokens you can receive from WETH:'); tokens.forEach(token => { console.log(`- ${token.symbol} on chain ${token.chainId}`); }); ``` ### Filter by Destination Chain ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactInputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ originChainId: 42161, // Arbitrum originTokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum destinationChainId: 8453 // Only show tokens on Base }) }); const { tokens } = await response.json(); console.log('Base tokens you can receive from Arbitrum USDC:'); tokens.forEach(token => { console.log(`- ${token.symbol}: ${token.address}`); }); ``` ### Build Destination Token Selector ```tsx theme={null} import { useEffect, useState } from 'react'; interface TokenInfo { chainId: number; address: string; name: string; symbol: string; decimals: number; logoUri?: string; featured: boolean; featureIndex: number; } interface DestinationSelectorProps { sourceChainId: number; sourceTokenAddress: string; onSelect: (token: TokenInfo) => void; } export const DestinationSelector = ({ sourceChainId, sourceTokenAddress, onSelect }: DestinationSelectorProps) => { const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); const [selectedChain, setSelectedChain] = useState(null); useEffect(() => { const params: Record = { originChainId: sourceChainId, originTokenAddress: sourceTokenAddress }; if (selectedChain) { params.destinationChainId = selectedChain; } fetch('https://trails-api.sequence.app/rpc/Trails/GetExactInputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify(params) }) .then(res => res.json()) .then(({ tokens }) => { // Sort by featured first, then by feature index const sorted = tokens.sort((a: TokenInfo, b: TokenInfo) => { if (a.featured && !b.featured) return -1; if (!a.featured && b.featured) return 1; return a.featureIndex - b.featureIndex; }); setTokens(sorted); setLoading(false); }); }, [sourceChainId, sourceTokenAddress, selectedChain]); if (loading) return
Loading destinations...
; // Group tokens by chain const byChain = tokens.reduce((acc, token) => { if (!acc[token.chainId]) acc[token.chainId] = []; acc[token.chainId].push(token); return acc; }, {} as Record); return (

Receive:

{Object.entries(byChain).map(([chainId, chainTokens]) => (

Chain {chainId}

{chainTokens.map(token => ( ))}
))}
); }; ``` ### Check Route Availability ```typescript theme={null} async function checkRoute( fromChain: number, fromToken: string, toChain: number, toToken: string ): Promise { const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactInputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ originChainId: fromChain, originTokenAddress: fromToken, destinationChainId: toChain, destinationTokenAddress: toToken }) }); const { tokens } = await response.json(); return tokens.length > 0; } // Usage const canSwap = await checkRoute( 42161, // Arbitrum '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC 8453, // Base '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC ); console.log('Route available:', canSwap); ``` ### Swap All Flow ```typescript theme={null} import { formatUnits } from 'viem'; async function buildSwapAllOptions( userAddress: string, sourceChainId: number, sourceToken: { address: string; symbol: string; decimals: number; balance: bigint } ) { // Get available destinations const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactInputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ originChainId: sourceChainId, originTokenAddress: sourceToken.address }) }); const { tokens } = await response.json(); console.log(`Swap all ${formatUnits(sourceToken.balance, sourceToken.decimals)} ${sourceToken.symbol} to:`); tokens.forEach(token => { console.log(`- ${token.symbol} on chain ${token.chainId}`); }); return tokens; } ``` ## When to Use Exact Input Use `GetExactInputRoutes` when: * User wants to spend a specific amount * "Swap all" or "Max" button functionality * User specifies the source amount * Portfolio liquidation flows Use `GetExactOutputRoutes` when: * User needs to receive a specific amount * Payment flows with fixed prices * Invoice payments Combine this endpoint with user balance data to show only routes for tokens they actually hold. ## Next Steps Find routes based on output amount Get a quote with EXACT\_INPUT trade type # GetExactOutputRoutes Source: https://docs.trails.build/api-reference/endpoints/get-exact-output-routes trails-api.gen.json post /rpc/Trails/GetExactOutputRoutes ## Overview The `GetExactOutputRoutes` endpoint retrieves available token routes for exact output swaps. Given a destination token, it returns all source tokens that can be swapped to achieve that exact output amount. This is essential for building UIs where users specify exactly how much they want to receive. ## Use Cases * Build "I want to receive exactly X tokens" interfaces * Find which source tokens can route to a specific destination * Payment flows where exact amounts are required * Invoice payments requiring precise token amounts * DeFi integrations with fixed deposit amounts ## Request Parameters ### Required Fields * **destinationChainId** (number): The chain ID of the destination token * **destinationTokenAddress** (string): The token address to receive ### Optional Fields * **originChainId** (number): Filter routes by source chain * **originTokenAddress** (string): Filter to routes from a specific token * **ownerAddress** (string): User's wallet address (for balance-aware filtering) ## Response The response includes: * **tokens** (TokenInfo\[]): Array of source tokens that can route to the destination ### TokenInfo Object Structure Each token object contains: * **chainId** (number): Chain where the token exists * **address** (string): Token contract address * **name** (string): Token name * **symbol** (string): Token symbol * **decimals** (number): Token decimals * **supportsBridging** (boolean, optional): Whether the token supports cross-chain bridging * **logoUri** (string, optional): URL to token logo * **featured** (boolean): Whether this is a featured/popular token * **featureIndex** (number): Sort order for featured tokens ## Examples ### Find Source Tokens for USDC on Base ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactOutputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ destinationChainId: 8453, // Base destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base }) }); const { tokens } = await response.json(); console.log('Tokens that can swap to USDC on Base:'); tokens.forEach(token => { console.log(`- ${token.symbol} on chain ${token.chainId}`); }); ``` ### Filter by Source Chain ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactOutputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ destinationChainId: 8453, // Base destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC originChainId: 1 // Only show routes from Ethereum mainnet }) }); const { tokens } = await response.json(); console.log('Ethereum tokens that can swap to USDC on Base:'); tokens.forEach(token => { console.log(`- ${token.symbol}: ${token.address}`); }); ``` ### Build Payment Source Selector ```tsx theme={null} import { useEffect, useState } from 'react'; interface TokenInfo { chainId: number; address: string; name: string; symbol: string; decimals: number; logoUri?: string; featured: boolean; } interface PaymentSourceSelectorProps { targetChainId: number; targetTokenAddress: string; userAddress: string; onSelect: (token: TokenInfo) => void; } export const PaymentSourceSelector = ({ targetChainId, targetTokenAddress, userAddress, onSelect }: PaymentSourceSelectorProps) => { const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch('https://trails-api.sequence.app/rpc/Trails/GetExactOutputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ destinationChainId: targetChainId, destinationTokenAddress: targetTokenAddress, ownerAddress: userAddress }) }) .then(res => res.json()) .then(({ tokens }) => { // Sort featured tokens first const sorted = tokens.sort((a: TokenInfo, b: TokenInfo) => { if (a.featured && !b.featured) return -1; if (!a.featured && b.featured) return 1; return 0; }); setTokens(sorted); setLoading(false); }); }, [targetChainId, targetTokenAddress, userAddress]); if (loading) return
Loading payment options...
; return (

Pay with:

{tokens.map(token => ( ))}
); }; ``` ### Check Specific Route Availability ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExactOutputRoutes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ destinationChainId: 8453, destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', originChainId: 42161, // Arbitrum originTokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' // USDC on Arbitrum }) }); const { tokens } = await response.json(); if (tokens.length > 0) { console.log('Route is available!'); } else { console.log('No direct route found'); } ``` ## When to Use Exact Output Use `GetExactOutputRoutes` when: * User needs to receive a specific amount (e.g., paying a \$100 invoice) * Protocol requires exact deposit amounts * NFT purchases with fixed prices * Subscription payments with fixed amounts Use `GetExactInputRoutes` when: * User wants to spend a specific amount * "Swap all" functionality * User specifies how much to send For the best UX, combine this with `GetTokenPrices` to show users the estimated cost in USD for each source token option. ## Next Steps Find routes based on input amount Get a quote with EXACT\_OUTPUT trade type # GetExchangeRate Source: https://docs.trails.build/api-reference/endpoints/get-exchange-rate trails-api.gen.json post /rpc/Trails/GetExchangeRate ## Overview The `GetExchangeRate` endpoint returns the current exchange rate for a fiat currency against USD. Use this to display fiat-denominated amounts in your UI or to convert token values to a user's local currency. ## Use Cases * Display token prices in local fiat currencies * Power fiat input mode in fund/withdraw flows * Build currency selectors with live exchange rates * Show USD-equivalent values in non-USD currencies ## Request Parameters ### Optional Fields * **toCurrency** (string): ISO 4217 currency code (e.g. `"EUR"`, `"GBP"`, `"JPY"`). Defaults to `"USD"` if omitted. ## Response * **exchangeRate** (`ExchangeRate`): Exchange rate object ### ExchangeRate Object Structure * **name** (string): Full name of the currency (e.g. `"Euro"`) * **symbol** (string): Currency symbol (e.g. `"€"`) * **value** (number): Exchange rate value relative to `vsCurrency` * **vsCurrency** (string): The base currency (typically `"USD"`) * **currencyType** (string): Currency type identifier ## Examples ### Get EUR Exchange Rate ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExchangeRate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ toCurrency: 'EUR' }) }); const { exchangeRate } = await response.json(); console.log(`1 USD = ${exchangeRate.value} ${exchangeRate.symbol}`); // e.g. "1 USD = 0.92 €" ``` ### Convert a USD Amount ```typescript theme={null} async function convertToLocal(usdAmount: number, currency: string): Promise { const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetExchangeRate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ toCurrency: currency }) }); const { exchangeRate } = await response.json(); const localAmount = usdAmount * exchangeRate.value; return `${exchangeRate.symbol}${localAmount.toFixed(2)}`; } const display = await convertToLocal(100, 'GBP'); console.log(display); // "£82.50" ``` Use `GetFiatCurrencyList` to retrieve the full list of supported currencies and their codes before calling this endpoint. ## Next Steps Get the list of supported fiat currencies Get USD prices for on-chain tokens # GetFiatCurrencyList Source: https://docs.trails.build/api-reference/endpoints/get-fiat-currency-list trails-api.gen.json post /rpc/Trails/GetFiatCurrencyList ## Overview The `GetFiatCurrencyList` endpoint returns the list of fiat currencies supported for onramp and fiat-denominated displays. Use this to populate currency selectors in fund flows and onramp integrations. ## Use Cases * Build a currency picker for onramp flows * Display flags and symbols for supported currencies * Validate that a user's local currency is supported before showing fiat input options ## Request Parameters This endpoint takes no parameters — send an empty request body. ## Response * **currencies** (`FiatCurrency[]`): Array of supported fiat currency objects ### FiatCurrency Object Structure Each currency includes: * **code** (string): ISO 4217 currency code (e.g. `"USD"`, `"EUR"`, `"GBP"`) * **symbol** (string): Currency symbol (e.g. `"$"`, `"€"`, `"£"`) * **name** (string): Full currency name (e.g. `"US Dollar"`) * **flag** (string): Emoji flag for the currency's primary country * **decimals** (number): Number of decimal places for display ## Examples ### Get All Supported Currencies ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetFiatCurrencyList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }); const { currencies } = await response.json(); currencies.forEach(c => { console.log(`${c.flag} ${c.name} (${c.code}): ${c.symbol}`); }); ``` ### Build a Currency Selector ```tsx theme={null} import { useEffect, useState } from 'react'; interface FiatCurrency { code: string; symbol: string; name: string; flag: string; decimals: number; } export const CurrencySelector = ({ onSelect }: { onSelect: (code: string) => void; }) => { const [currencies, setCurrencies] = useState([]); useEffect(() => { fetch('https://trails-api.sequence.app/rpc/Trails/GetFiatCurrencyList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }) .then(res => res.json()) .then(({ currencies }) => setCurrencies(currencies)); }, []); return ( ); }; ``` ## Next Steps Get live exchange rates for a currency Use fiat currencies in the fund widget # GetIntent Source: https://docs.trails.build/api-reference/endpoints/get-intent trails-api.gen.json post /rpc/Trails/GetIntent ## Overview The `GetIntent` endpoint retrieves the complete intent object for a specific intent ID. This provides all the details about the intent including quote information, fee breakdown, transaction calls, and metadata. ## Use Cases * Retrieve full intent details after creation * Review quote and fee information * Inspect transaction calls and preconditions * Verify intent parameters * Debug issues with intent execution * Display intent details in UI ## Request Parameters ### Required Fields * **intentId** (string): The unique identifier of the intent ## Response The response includes: * **intent** (Intent): Complete intent object with all details ### Intent Object Structure The intent object contains comprehensive information: #### Core Fields * **id** (number): Internal database ID * **intentId** (string): Unique intent identifier * **status** (IntentStatus): Current status * **ownerAddress** (string): Wallet address of the intent owner * **originIntentAddress** (string): Intent contract address on origin chain * **destinationIntentAddress** (string): Intent contract address on destination chain * **destinationSalt** (number): Salt for deterministic intent address generation * **trailsVersion** (string): Version of Trails protocol used * **expiresAt** (string): Intent expiration timestamp * **createdAt** (string): Creation timestamp * **updatedAt** (string): Last update timestamp #### Quote Request * **quoteRequest** (QuoteIntentRequest): Original quote parameters * Origin and destination chain/token details * Amounts and trade type * Options (provider, slippage, etc.) #### Transaction Details * **depositTransaction** (DepositTransaction): Deposit details * Token address and amount * Recipient address * Token decimals * **calls** (IntentCalls\[]): Cross-chain calls to execute * Chain ID for each call set * Transaction calls with calldata * Nonce and space information * **preconditions** (TransactionPrecondition\[]): Pre-execution checks * Token balance requirements * Minimum amount validations * **metaTxns** (MetaTxn\[]): Meta-transaction information * Meta-transaction IDs * Wallet addresses * Contract targets #### Quote Information * **quote** (IntentProviderQuote): Quote provider details * Provider name and request ID * Input/output amounts and minimums * Slippage and price impact * Provider fees in USD #### Fee Breakdown * **fees** (IntentFees): Comprehensive fee information * **originGas**: Origin chain gas fees * **destinationGas**: Destination chain gas fees * **provider**: Provider fees (quote provider + Trails) * **feeTokenAddress**: Token used for fees * **feeTokenTotal**: Total fee in fee token * **totalFeeAmount**: Total fee amount * **totalFeeUsd**: Total fee in USD ## Example ```typescript theme={null} const intentResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/GetIntent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_123abc' }) }); const { intent } = await intentResponse.json(); console.log('Intent ID:', intent.intentId); console.log('Status:', intent.status); console.log('Owner:', intent.ownerAddress); console.log('Expires:', intent.expiresAt); // Quote information console.log('From Amount:', intent.quote.fromAmount); console.log('To Amount:', intent.quote.toAmount); console.log('Price Impact:', intent.quote.priceImpact); // Fee breakdown console.log('Total Fee (USD):', intent.fees.totalFeeUsd); console.log('Origin Gas Fee:', intent.fees.originGas.totalFeeUsd); console.log('Destination Gas Fee:', intent.fees.destinationGas?.totalFeeUsd); console.log('Provider Fee:', intent.fees.provider.totalFeeUsd); ``` ## Detailed Fee Inspection ```typescript theme={null} function analyzeFees(intent: Intent) { const fees = intent.fees; console.log('=== Fee Breakdown ==='); // Gas fees console.log('\nOrigin Chain Gas:'); console.log(' Chain ID:', fees.originGas.chainId); console.log(' Gas Limit:', fees.originGas.totalGasLimit); console.log(' Gas Price:', fees.originGas.gasPrice); console.log(' Native Token:', fees.originGas.nativeTokenSymbol); console.log(' Cost (USD):', fees.originGas.totalFeeUsd); console.log(' Network Status:', fees.originGas.chainGasUsageStatus); console.log('\nDestination Chain Gas:'); console.log(' Chain ID:', fees.destinationGas?.chainId); console.log(' Gas Limit:', fees.destinationGas?.totalGasLimit); console.log(' Gas Price:', fees.destinationGas?.gasPrice); console.log(' Native Token:', fees.destinationGas?.nativeTokenSymbol); console.log(' Cost (USD):', fees.destinationGas?.totalFeeUsd); console.log(' Network Status:', fees.destinationGas?.chainGasUsageStatus); // Provider fees console.log('\nProvider Fees:'); console.log(' Quote Provider:', fees.provider.quoteProvider); console.log(' Provider Fee:', fees.provider.quoteProviderFeeUsd, 'USD'); console.log(' Trails Fee:', fees.provider.trailsFeeUsd, 'USD'); console.log(' Total Provider:', fees.provider.totalFeeUsd, 'USD'); // Grand total console.log('\n=== TOTAL ==='); console.log('Fee Token:', fees.feeTokenAddress); console.log('Total Amount:', fees.totalFeeAmount); console.log('Total USD:', fees.totalFeeUsd); } const { intent } = await getIntent('intent_123abc'); analyzeFees(intent); ``` ## Inspecting Transaction Calls ```typescript theme={null} function inspectCalls(intent: Intent) { console.log('=== Transaction Origin Calls ===\n'); console.log('Origin Chain ID:', intent.originCalls.chainId); console.log('Origin Space:', intent.originCalls.space); console.log('Origin Nonce:', intent.originCalls.nonce); console.log('Origin Calls:', intent.originCalls.calls.length); intent.originCalls.calls.forEach((call, callIndex) => { console.log(`\n Call ${callIndex + 1}:`); console.log(' To:', call.to); console.log(' Value:', call.value); console.log(' Data:', call.data); console.log(' Gas Limit:', call.gasLimit); console.log(' Delegate:', call.delegateCall); }); console.log('=== Transaction Destination Calls ===\n'); console.log('Destination Chain ID:', intent.destinationCalls?.chainId); console.log('Destination Space:', intent.destinationCalls?.space); console.log('Destination Nonce:', intent.destinationCalls?.nonce); console.log('Destination Calls:', intent.destinationCalls?.calls.length); intent.destinationCalls?.calls.forEach((call, callIndex) => { console.log(`\n Call ${callIndex + 1}:`); console.log(' To:', call.to); console.log(' Value:', call.value); console.log(' Data:', call.data); console.log(' Gas Limit:', call.gasLimit); console.log(' Delegate:', call.delegateCall); }); } const { intent } = await getIntent('intent_123abc'); inspectCalls(intent); ``` ## Use Cases ### 1. Display Intent Summary in UI ```tsx theme={null} import { TrailsApi, type Intent } from "@0xtrails/api"; import { useEffect, useState } from "react"; const trailsApi = new TrailsApi('YOUR_API_KEY') export const IntentSummary = ({ intentId }: { intentId: string }) => { const [intent, setIntent] = useState(null); useEffect(() => { trailsApi.getIntent({ intentId }).then(({ intent }) => setIntent(intent)); }, [intentId]); if (!intent) return
Loading...
; return (

Intent {intent.intentId}

Status: {intent.status}
From: Chain {intent.quoteRequest.originChainId}
To: Chain {intent.quoteRequest.destinationChainId}
Amount: {intent.quote.fromAmount} → {intent.quote.toAmount}
Total Fee: ${intent.fees.totalFeeUsd}
Expires: {new Date(intent.expiresAt).toLocaleString()}
); } ``` ### 2. Verify Intent Before Signing ```typescript theme={null} import { TrailsApi } from "@0xtrails/api"; const trailsApi = new TrailsApi('YOUR_API_KEY') export async function verifyBeforeSigning(intentId: string) { const { intent } = await trailsApi.getIntent({ intentId }); // Check expiration const expiresAt = new Date(intent.expiresAt); const now = new Date(); if (expiresAt < now) { throw new Error('Intent has expired'); } // Check fees are reasonable if (intent.fees.totalFeeUsd > 10) { console.warn('High fees detected:', intent.fees.totalFeeUsd); } const slippage = (intent.quote.fromAmount - intent.quote.fromAmountMin) / intent.quote.fromAmount; // Check if slippage is greater than 5% if (slippage > 0.05) { console.warn('High slippage:', slippage * BigInt(100), '%'); } return intent; } ``` Use this endpoint to retrieve intent details at any time. The intent object remains accessible even after execution completes. ## Next Steps * Use `GetIntentReceipt` to check execution status and get transaction hashes * Use `SearchIntents` to find all intents for a specific address * Modify and recommit expired intents with fresh quotes # GetIntentHistory Source: https://docs.trails.build/api-reference/endpoints/get-intent-history trails-api.gen.json post /rpc/Trails/GetIntentHistory ## Overview The `GetIntentHistory` endpoint provides a paginated view of intent history with detailed receipt information. This endpoint returns full `IntentHistory` objects that include the complete `IntentReceipt` for each intent, giving you comprehensive transaction data including status, timestamps, and transaction hashes. ## Use Cases * Build comprehensive transaction history pages with full receipt details * Display user transaction history with complete status information * Create audit logs with detailed transaction information * Export complete transaction data for analytics * Build dashboards with full intent lifecycle visibility ## Request Parameters All parameters are optional: * **page** (Page): Pagination configuration * **column** (string): Column to paginate by (typically `"id"` or `"createdAt"`) * **before** (number): Cursor for previous page * **after** (number): Cursor for next page * **sort** (SortBy\[]): Sorting configuration * **column** (string): Column to sort by * **order** (SortOrder): `DESC` or `ASC` * **pageSize** (number): Number of results per page (default: 20) * **more** (boolean): Indicates if more pages are available * **byProjectId** (number): Filter by project ID * **byOwnerAddress** (string): Filter by wallet address * **byStatus** (IntentStatus): Filter to a single status * **byStatuses** (IntentStatus\[]): Filter to multiple statuses (takes precedence over `byStatus`) * **originChainId** (number): Filter by origin (source) chain ID * **destinationChainId** (number): Filter by destination chain ID * **fromTime** (number): Unix timestamp — return only intents created at or after this time * **toTime** (number): Unix timestamp — return only intents created at or before this time * **includeBalances** (boolean): When `true`, augment each history entry with on-chain balance data for the intent-relevant tokens * **onlyRecoverable** (boolean): When `true`, return only intents that have recoverable (stuck) balances ## Response The response includes: * **intents** (IntentHistory\[]): Array of intent history objects with receipts * **nextPage** (Page): Pagination cursor for the next page ### IntentHistory Structure Each `IntentHistory` object contains: * **intentId** (string): Unique intent identifier * **status** (IntentStatus): Current status (`QUOTED`, `COMMITTED`, `EXECUTING`, `FAILED`, `SUCCEEDED`, `ABORTED`, `REFUNDED`, `INVALID`) * **expiresAt** (string): Intent expiration timestamp * **updatedAt** (string): Last update timestamp * **createdAt** (string): Creation timestamp * **receipt** (IntentReceipt): Complete receipt with transaction details ### IntentReceipt Details The receipt object includes comprehensive transaction information: * **id** (number): Internal database ID * **intentId** (string): Unique intent identifier * **status** (IntentStatus): Current status * **ownerAddress** (string): Wallet address that initiated the intent * **originChainId** (number): Source chain ID * **destinationChainId** (number): Destination chain ID * **depositTransaction** (IntentTransaction): Deposit transaction details * **originTransaction** (IntentTransaction): Origin chain transaction details * **destinationTransaction** (IntentTransaction): Destination chain transaction details * **summary** (IntentReceiptSummary): Summary with token metadata and route providers ## Examples ### Basic Request (First Page) ```typescript theme={null} const historyResponse = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetIntentHistory', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ page: { pageSize: 20, sort: [{ column: 'createdAt', order: 'DESC' }] } }) } ); const { intents, nextPage } = await historyResponse.json(); console.log(`Loaded ${intents.length} intents with receipts`); intents.forEach(intent => { console.log(`${intent.intentId}: ${intent.status}`); console.log(` Receipt status: ${intent.receipt.status}`); console.log(` Origin TX: ${intent.receipt.originTransaction?.txnHash}`); console.log(` Destination TX: ${intent.receipt.destinationTransaction?.txnHash}`); }); // Check if there are more pages if (nextPage) { console.log('More pages available'); } ``` ### Filter by Owner Address ```typescript theme={null} const historyResponse = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetIntentHistory', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOwnerAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', page: { pageSize: 50, sort: [{ column: 'createdAt', order: 'DESC' }] } }) } ); const { intents } = await historyResponse.json(); console.log(`Found ${intents.length} intents for this address`); ``` ### Paginated Loading with Full Receipts ```typescript theme={null} import { SortOrder, type IntentHistory, type Page } from "@0xtrails/api"; async function loadAllHistoryWithReceipts(ownerAddress: string) { const allIntents: IntentHistory[] = []; let page: Page | undefined = { pageSize: 50, sort: [{ column: 'createdAt', order: SortOrder.DESC }] }; while (page) { const { intents, nextPage }: { intents: IntentHistory[], nextPage: Page } = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetIntentHistory', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOwnerAddress: ownerAddress, page }) } ).then(r => r.json()); allIntents.push(...intents); page = nextPage; console.log(`Loaded ${allIntents.length} intents so far...`); } return allIntents; } const allHistory = await loadAllHistoryWithReceipts('0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1'); console.log(`Total intents: ${allHistory.length}`); ``` ### Transaction History Component with Receipts ```tsx theme={null} import { SortOrder, type IntentHistory, type Page } from '@0xtrails/api'; import { useState, useEffect } from 'react'; function getExplorerUrl(chainId: number, txHash: string) { const explorers: Record = { 1: 'https://etherscan.io/tx/', 137: 'https://polygonscan.com/tx/', 8453: 'https://basescan.org/tx/', 42161: 'https://arbiscan.io/tx/', 10: 'https://optimistic.etherscan.io/tx/' }; return (explorers[chainId] || '') + txHash; } export const IntentHistoryWithReceipts = ({ ownerAddress }: { ownerAddress: string }) => { const [intents, setIntents] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { async function loadHistory() { try { const response = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetIntentHistory', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOwnerAddress: ownerAddress, page: { pageSize: 20, sort: [{ column: 'createdAt', order: SortOrder.DESC }] } }) } ); const data = await response.json(); setIntents(data.intents); } catch (error) { console.error('Failed to load intent history:', error); } finally { setLoading(false); } } loadHistory(); }, [ownerAddress]); if (loading) return
Loading...
; return (

Transaction History

{intents.length === 0 ? (

No transactions found

) : (
    {intents.map(intent => (
  • {intent.intentId.slice(0, 12)}... {intent.status}
    Chain {intent.receipt.originChainId} → Chain {intent.receipt.destinationChainId}
    {intent.receipt.summary && (
    {intent.receipt.summary.originTokenMetadata?.symbol} →{' '} {intent.receipt.summary.destinationTokenMetadata?.symbol}
    )}
    {new Date(intent.createdAt || '').toLocaleString()}
    {intent.receipt.originTransaction?.txnHash && ( View Origin TX )} {intent.receipt.destinationTransaction?.txnHash && ( View Destination TX )}
  • ))}
)}
); } ``` ### Analyze Intent History ```typescript theme={null} import { TrailsApi, type IntentHistory } from '@0xtrails/api' const trailsApi = new TrailsApi('YOUR_API_KEY') async function analyzeIntentHistory(ownerAddress: string) { const { intents } = await trailsApi.getIntentHistory({ byOwnerAddress: ownerAddress, page: { pageSize: 100 } }); const stats = { total: intents.length, succeeded: intents.filter((i: IntentHistory) => i.status === 'SUCCEEDED').length, failed: intents.filter((i: IntentHistory) => i.status === 'FAILED').length, aborted: intents.filter((i: IntentHistory) => i.status === 'ABORTED').length, refunded: intents.filter((i: IntentHistory) => i.status === 'REFUNDED').length, pending: intents.filter((i: IntentHistory) => ['QUOTED', 'COMMITTED', 'EXECUTING'].includes(i.status) ).length, chains: [...new Set(intents.flatMap((i: IntentHistory) => [i.receipt.originChainId, i.receipt.destinationChainId] ))], routeProviders: [...new Set(intents.flatMap((i: IntentHistory) => i.receipt.summary?.routeProviders || [] ))] }; return stats; } const stats = await analyzeIntentHistory('0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1'); console.log('Intent History Analysis:', stats); ``` `GetIntentHistory` provides comprehensive transaction data including full receipt details, transaction hashes, and token metadata. It's ideal for building detailed transaction history views, audit logs, and analytics dashboards. ## Best Practices 1. **Use appropriate page sizes**: Since responses include full receipts, use smaller page sizes (10-30) to reduce payload size 2. **Cache results**: Store intent history locally to avoid refetching 3. **Filter by owner address**: Always filter by owner address when possible to reduce result set 4. **Handle all status types**: Account for `ABORTED` and `REFUNDED` statuses in addition to `SUCCEEDED` and `FAILED` Always handle the case where `nextPage` is `null`, indicating no more results are available. ## Next Steps * Use `GetIntent` to fetch the full intent object (not just receipt) when needed * Use `GetIntentReceipt` for single intent receipt lookups * Use `SearchIntents` to find intents by various criteria # GetIntentReceipt Source: https://docs.trails.build/api-reference/endpoints/get-intent-receipt trails-api.gen.json post /rpc/Trails/GetIntentReceipt ## Overview The `GetIntentReceipt` endpoint retrieves the receipt for a specific intent, providing detailed information about all transactions involved in the cross-chain execution. ## Use Cases * Check the status of an executing intent * Retrieve transaction hashes for block explorer links * Monitor cross-chain transaction progress * Get detailed transaction information for analytics * Verify successful completion ## Request Parameters ### Required Fields * **intentId** (string): The unique identifier of the intent ## Response The response includes: * **intentReceipt** (IntentReceipt): Complete receipt with transaction details ### IntentReceipt Structure * **id** (number): Internal database ID * **intentId** (string): Unique intent identifier * **status** (IntentStatus): Current status (`QUOTED`, `COMMITTED`, `EXECUTING`, `FAILED`, `SUCCEEDED`) * **ownerAddress** (string): Wallet address that initiated the intent * **originChainId** (number): Source chain ID * **destinationChainId** (number): Destination chain ID * **depositTransactionId** (number): ID of deposit transaction * **depositTransaction** (IntentTransaction): Deposit transaction details * **originTransactionId** (number): ID of origin transaction * **originTransaction** (IntentTransaction): Origin chain transaction details * **destinationTransactionId** (number): ID of destination transaction * **destinationTransaction** (IntentTransaction): Destination chain transaction details * **updatedAt** (string): Last update timestamp * **createdAt** (string): Creation timestamp ### IntentTransaction Details Each transaction object includes: * **id** (number): Transaction database ID * **intentId** (string): Associated intent ID * **chainId** (number): Chain where transaction occurred * **fromAddress** (string): Sender address * **toAddress** (string): Recipient address * **tokenAddress** (string): Token contract address * **tokenAmount** (number): Amount transferred * **txnHash** (string): Transaction hash for block explorer * **status** (TransactionStatus): Transaction status * `UNKNOWN`: Initial state * `PENDING`: Waiting to be mined * `RELAYING`: Being relayed * `RELAYED`: Relayed successfully * `MINING`: Being mined * `SUCCEEDED`: Confirmed on-chain * `FAILED`: Transaction failed * **statusReason** (string): Reason for failure (if failed) * **calldata** (string): Transaction calldata * **metaTxnId** (string): Meta-transaction ID (if gasless) * **precondition** (TransactionPrecondition): Pre-execution conditions * **depositIntentEntry** (DepositIntentEntry): Deposit signature details ## Example ```typescript theme={null} const receiptResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/GetIntentReceipt', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_123abc' }) }); const { intentReceipt } = await receiptResponse.json(); console.log('Status:', intentReceipt.status); console.log('Deposit TX:', intentReceipt.depositTransaction.txnHash); console.log('Origin TX:', intentReceipt.originTransaction.txnHash); console.log('Destination TX:', intentReceipt.destinationTransaction.txnHash); ``` ## Polling Pattern For real-time updates, poll this endpoint: ```typescript theme={null} import { TrailsApi } from '@0xtrails/api' const trailsApi = new TrailsApi('YOUR_API_KEY') async function pollReceipt(intentId: string, maxAttempts = 60) { for (let i = 0; i < maxAttempts; i++) { const { intentReceipt } = await trailsApi.getIntentReceipt({ intentId }); console.log(`Attempt ${i + 1}: ${intentReceipt.status}`); if (intentReceipt.status === 'SUCCEEDED') { return intentReceipt; } if (intentReceipt.status === 'FAILED') { throw new Error(intentReceipt.originTransaction.statusReason); } // Wait 2 seconds before next poll await new Promise(resolve => setTimeout(resolve, 2000)); } throw new Error('Timeout waiting for intent completion'); } const receipt = await pollReceipt('intent_123abc'); console.log('Completed!', receipt); ``` ## Block Explorer Links Use transaction hashes to create explorer links: ```typescript theme={null} function getExplorerUrl(chainId: number, txHash: string) { const explorers = { 1: 'https://etherscan.io/tx/', 137: 'https://polygonscan.com/tx/', 8453: 'https://basescan.org/tx/', 42161: 'https://arbiscan.io/tx/', 10: 'https://optimistic.etherscan.io/tx/' }; return explorers[chainId] + txHash; } const { intentReceipt } = await trailsApi.getIntentReceipt({ intentId }); console.log('Origin TX:', getExplorerUrl( intentReceipt.originChainId, intentReceipt.originTransaction.txnHash )); ``` For streaming updates without polling, consider using `WaitIntentReceipt` instead. ## Next Steps * Use transaction hashes to verify on block explorers * Check destination wallet balance to confirm receipt * Use `GetIntent` to retrieve the original intent details * Use `SearchIntents` to find related transactions # GetProtocolContracts Source: https://docs.trails.build/api-reference/endpoints/get-protocol-contracts trails-api.gen.json post /rpc/Trails/GetProtocolContracts ## Overview `GetProtocolContracts` returns the deployed Trails contract addresses for a given intent-protocol version. Use it when a client needs the router / entrypoint / router-shim / utils addresses for the version it is about to commit an intent against, instead of hardcoding an address that may drift when a new protocol version rolls out. This replaces the deprecated `GetTrailsContracts` endpoint. `GetProtocolContracts` accepts an explicit `intentProtocol` parameter, so a single client can serve multiple protocol versions concurrently. ## Request Parameters | Field | Type | Required | Description | | - | - | - | - | | `intentProtocol` | `IntentProtocolVersion` | No | Protocol version to resolve contracts for. Values: `v1`, `v1_5`. Defaults to the API's [current default](/api-reference/endpoints/get-default-intent-protocol) when omitted. | ## Response * **TrailsContracts** (object) * **trailsIntentEntrypointAddress** (string) * **trailsRouterAddress** (string) * **trailsRouterShimAddress** (string) * **trailsUtilsAddress** (string) The response is a single object; addresses are deterministic across chains via the ERC-2470 Singleton Factory. ## Example ```typescript theme={null} const response = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetProtocolContracts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ intentProtocol: 'v1_5' }), }, ) const { TrailsContracts } = await response.json() console.log('Entrypoint:', TrailsContracts.trailsIntentEntrypointAddress) console.log('Utils:', TrailsContracts.trailsUtilsAddress) ``` ## Next Steps List every supported intent-protocol version Discover the API's current default version # GetSupportedIntentProtocols Source: https://docs.trails.build/api-reference/endpoints/get-supported-intent-protocols trails-api.gen.json post /rpc/Trails/GetSupportedIntentProtocols ## Overview `GetSupportedIntentProtocols` returns the intent-protocol versions the Trails API currently serves. Each version identifies a different orchestration + contracts stack; a quote produced for `v1_5` cannot be committed against `v1` and vice versa. Use this alongside [`GetDefaultIntentProtocol`](/api-reference/endpoints/get-default-intent-protocol) and [`GetProtocolContracts`](/api-reference/endpoints/get-protocol-contracts) to negotiate the correct version at runtime instead of hardcoding it. ## Request `GetSupportedIntentProtocols` takes no parameters. ## Response * **versions** (`IntentProtocolVersion[]`) — supported versions in no particular order. Members: `v1`, `v1_5`. ## Example ```typescript theme={null} const response = await fetch( 'https://trails-api.sequence.app/rpc/Trails/GetSupportedIntentProtocols', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({}), }, ) const { versions } = await response.json() console.log('Supported intent protocols:', versions) ``` ## Next Steps Get the API's current default protocol version Resolve on-chain contract addresses for a protocol version # GetTokenList Source: https://docs.trails.build/api-reference/endpoints/get-token-list trails-api.gen.json post /rpc/Trails/GetTokenList ## Overview The `GetTokenList` endpoint retrieves a list of tokens available on specified chains. This is useful for building token selection UIs, searching for tokens, and discovering available assets for swaps and transfers. ## Use Cases * Build token selector dropdowns * Implement token search functionality * Display featured/popular tokens * Filter tokens by chain * Look up specific token addresses * Exclude certain tokens from selection ## Request Parameters ### Required Fields * **chainIds** (number\[]): Array of chain IDs to get tokens for ### Optional Fields * **searchQuery** (string): Search tokens by name, symbol, or address * **limit** (number): Maximum number of tokens to return * **tokenAddress** (string): Filter to a specific token address * **includeAllListed** (boolean): Include all listed tokens, not just featured * **includeExternal** (boolean): Include external/non-native tokens * **excludeTokens** (string\[]): Array of token addresses to exclude ## Response The response includes: * **tokens** (TokenInfo\[]): Array of token information ### TokenInfo Object Structure Each token object contains: * **chainId** (number): Chain where the token exists * **address** (string): Token contract address * **name** (string): Token name * **symbol** (string): Token symbol * **decimals** (number): Token decimals * **supportsBridging** (boolean, optional): Whether the token supports bridging * **logoUri** (string, optional): URL to token logo * **featured** (boolean): Whether this is a featured/popular token * **featureIndex** (number): Sort order for featured tokens ## Examples ### Get Featured Tokens for Multiple Chains ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: [1, 8453, 42161], // Ethereum, Base, Arbitrum limit: 20 }) }); const { tokens } = await response.json(); console.log('Featured tokens:'); tokens.forEach(token => { console.log(`${token.symbol} on chain ${token.chainId}: ${token.address}`); }); ``` ### Search for a Token ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: [1, 8453, 42161, 137, 10], searchQuery: 'USDC', includeAllListed: true }) }); const { tokens } = await response.json(); console.log('USDC tokens found:'); tokens.forEach(token => { console.log(`Chain ${token.chainId}: ${token.address}`); }); ``` ### Look Up Specific Token ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: [8453], // Base tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC }) }); const { tokens } = await response.json(); if (tokens.length > 0) { const token = tokens[0]; console.log(`Found: ${token.name} (${token.symbol})`); console.log(`Decimals: ${token.decimals}`); console.log(`Supports bridging: ${token.supportsBridging}`); } ``` ### Build Token Selector with Search ```tsx theme={null} import { useEffect, useState, useCallback } from 'react'; import { debounce } from 'lodash'; interface TokenInfo { chainId: number; address: string; name: string; symbol: string; decimals: number; logoUri?: string; featured: boolean; } interface TokenSelectorProps { chainIds: number[]; onSelect: (token: TokenInfo) => void; excludeAddresses?: string[]; } export const TokenSelector = ({ chainIds, onSelect, excludeAddresses = [] }: TokenSelectorProps) => { const [tokens, setTokens] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(true); const fetchTokens = useCallback(async (query: string) => { setLoading(true); const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds, searchQuery: query || undefined, includeAllListed: !!query, excludeTokens: excludeAddresses, limit: 50 }) }); const { tokens } = await response.json(); setTokens(tokens); setLoading(false); }, [chainIds, excludeAddresses]); // Debounced search const debouncedSearch = useCallback( debounce((query: string) => fetchTokens(query), 300), [fetchTokens] ); useEffect(() => { fetchTokens(''); }, [fetchTokens]); useEffect(() => { if (searchQuery) { debouncedSearch(searchQuery); } }, [searchQuery, debouncedSearch]); return (
setSearchQuery(e.target.value)} /> {loading ? (
Loading...
) : (
    {tokens.map(token => (
  • onSelect(token)} > {token.logoUri && ( {token.symbol} )} {token.symbol} {token.name} Chain {token.chainId}
  • ))}
)}
); }; ``` ### Get All Tokens Including External ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: [8453], // Base includeAllListed: true, includeExternal: true, limit: 100 }) }); const { tokens } = await response.json(); console.log(`Found ${tokens.length} tokens on Base`); ``` ### Exclude Specific Tokens ```typescript theme={null} // Exclude native tokens from the list const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenList', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ chainIds: [1], excludeTokens: [ '0x0000000000000000000000000000000000000000', // Native ETH '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' // WETH ], limit: 20 }) }); const { tokens } = await response.json(); ``` ## Filtering Tips | Use Case | Parameters | | - | - | | Featured tokens only | Default (no extra params) | | All tokens | `includeAllListed: true` | | Search by name/symbol | `searchQuery: "USDC"` | | Specific token lookup | `tokenAddress: "0x..."` | | Exclude tokens | `excludeTokens: ["0x...", "0x..."]` | Featured tokens are curated for popularity and liquidity. Use `includeAllListed` for comprehensive token lists. ## Next Steps Get supported chains for token selection Get USD prices for selected tokens # GetTokenPrices Source: https://docs.trails.build/api-reference/endpoints/get-token-prices trails-api.gen.json post /rpc/Trails/GetTokenPrices ## Overview The `GetTokenPrices` endpoint retrieves current USD prices for one or more tokens. This is useful for displaying real-time token values, calculating transaction costs, and showing users estimated amounts in fiat currency. ## Use Cases * Display token values in USD in your UI * Calculate total transaction costs in fiat * Show users how much they'll receive in USD * Build portfolio value displays * Create price alerts or notifications * Compare token values across chains ## Request Parameters ### Required Fields * **tokens** (Token\[]): Array of token objects to get prices for ### Token Object Structure Each token object should include: * **chainId** (number): The chain ID where the token exists * **tokenAddress** (string): The token contract address * **tokenSymbol** (string, optional): Token symbol for reference ## Response The response includes: * **tokenPrices** (TokenPrice\[]): Array of token price objects ### TokenPrice Object Structure Each price object contains: * **token** (Token): The token information * **chainId** (number): Chain ID * **tokenAddress** (string): Token contract address * **tokenSymbol** (string): Token symbol * **priceUsd** (number): Current price in USD * **updatedAt** (string): Timestamp of last price update ## Examples ### Single Token Price ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenPrices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokens: [ { chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum tokenSymbol: 'USDC' } ] }) }); const { tokenPrices } = await response.json(); console.log('USDC Price:', tokenPrices[0].priceUsd); console.log('Updated:', tokenPrices[0].updatedAt); ``` ### Multiple Token Prices ```typescript theme={null} import type { TokenPrice } from "@0xtrails/api"; const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenPrices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokens: [ { chainId: 1, tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH on Ethereum tokenSymbol: 'WETH' }, { chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum tokenSymbol: 'USDC' }, { chainId: 137, tokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon tokenSymbol: 'USDC' } ] }) }); const { tokenPrices } = await response.json(); tokenPrices.forEach((price: TokenPrice) => { console.log(`${price.token.tokenSymbol} on Chain ${price.token.chainId}:`, `$${price.priceUsd}`); }); ``` ## Calculate Transaction Cost ```typescript theme={null} import type { TokenPrice } from "@0xtrails/api"; const response = await fetch('https://trails-api.sequence.app/rpc/Trails/GetTokenPrices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokens: [ { chainId: 1, tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH on Ethereum tokenSymbol: 'WETH' }, { chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum tokenSymbol: 'USDC' }, { chainId: 137, tokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon tokenSymbol: 'USDC' } ] }) }); const { tokenPrices } = await response.json(); tokenPrices.forEach((price: TokenPrice) => { console.log(`${price.token.tokenSymbol} on Chain ${price.token.chainId}:`, `$${price.priceUsd}`); }); ``` ## Common Token Addresses ### Ethereum (chainId: 1) * **WETH**: `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` * **USDC**: `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` * **USDT**: `0xdAC17F958D2ee523a2206206994597C13D831ec7` * **DAI**: `0x6B175474E89094C44Da98b954EedeAC495271d0F` ### Polygon (chainId: 137) * **WMATIC**: `0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270` * **USDC**: `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` * **USDT**: `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` ### Base (chainId: 8453) * **WETH**: `0x4200000000000000000000000000000000000006` * **USDC**: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` ### Arbitrum (chainId: 42161) * **WETH**: `0x82aF49447D8a07e3bd95BD0d56f35241523fBab1` * **USDC**: `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` ## Notes * Not all tokens have price data available * The `updatedAt` timestamp indicates when the price was last refreshed * Prices are returned in USD ## Next Steps Use token prices to display quote values in USD Calculate total transaction costs with token prices # PrepareIntentRecovery Source: https://docs.trails.build/api-reference/endpoints/prepare-intent-recovery trails-api.gen.json post /rpc/Trails/PrepareIntentRecovery ## Overview The `PrepareIntentRecovery` endpoint returns a backend-prepared payload and typed data that the user signs to recover funds stranded in an intent wallet. Combined with [`BuildIntentRecoveryTransaction`](/api-reference/endpoints/build-intent-recovery-transaction), this endpoint enables clients to drain recoverable balances from origin or destination intent addresses after an intent has failed, expired, or otherwise left funds behind. ## Use Cases * Recover an intent's origin deposit when the route became unfillable * Sweep dust or leftover balances from an intent wallet * Refund a user from a stranded intent address ## Request Parameters ### Required Fields * **intentId** (string): The unique ID of the intent to recover. ### Optional Fields * **intentAddress** (string): If omitted, the backend auto-selects the origin or destination intent address by recoverable balance. * **refundToAddress** (string): Defaults to the intent owner address when omitted. * **tokenAddress** (string): Constrain recovery token selection when multiple recoverable tokens exist. ## Response * **intentId** (string) * **intentProtocol** (`IntentProtocolVersion`) * **intentAddress** (string): The intent wallet being recovered. * **chainId** (number): Chain ID of the intent wallet. * **intentSource** (string): `"origin"` or `"destination"` — which intent wallet was selected for recovery. * **recoveryTokens** (array of `IntentTokenBalance`, optional): The token balances eligible for recovery. * **payload** (any): Payload to be signed. * **typedData** (any): EIP-712 typed data for client signing. * **payloadHash** (string) ## Flow 1. Call `PrepareIntentRecovery` to get the payload and typed data. 2. Have the user sign the typed data with their wallet. 3. Call [`BuildIntentRecoveryTransaction`](/api-reference/endpoints/build-intent-recovery-transaction) with the signed payload to receive the executable transaction. 4. Broadcast the transaction. ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/PrepareIntentRecovery', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_abc123' }) }); const { typedData, payload, payloadHash, intentAddress } = await response.json(); // Sign typedData with the user's wallet, then continue with BuildIntentRecoveryTransaction const signature = await walletClient.signTypedData(typedData); ``` ## Next Steps Build the executable recovery transaction Notify Trails of an on-chain abort # QuoteIntent Source: https://docs.trails.build/api-reference/endpoints/quote-intent trails-api.gen.json post /rpc/Trails/QuoteIntent ## Overview The `QuoteIntent` endpoint allows you to request a quote for a cross-chain intent transaction. It provides comprehensive information about the transaction including gas fees, optimal routing, and price impact. ## Use Cases * Get a quote for swapping tokens across different chains * Estimate fees before executing a cross-chain transaction * Compare different routing options * Calculate price impact and slippage ## Request Parameters ### Required Fields * **ownerAddress** (string): The wallet address initiating the transaction * **originChainId** (number): Source chain ID where the transaction originates * **originTokenAddress** (string): Contract address of the source token * **destinationChainId** (number): Target chain ID for the transaction * **destinationTokenAddress** (string): Contract address of the destination token * **destinationToAddress** (string): Recipient address on the destination chain ### Optional Fields * **tradeType** (TradeType): Either `EXACT_INPUT` or `EXACT_OUTPUT` * **destinationTokenAmount** (bigint): Amount of destination tokens (only for EXACT\_OUTPUT) * **originTokenAmount** (bigint): Amount of origin tokens (only for EXACT\_INPUT) * **destinationToAddress** (string): Recipient address on the destination chain (defaults to `ownerAddress`) * **destinationApproveAddress** (string): Contract address to pre-approve on the destination chain before executing `destinationCallData` * **destinationCallData** (string): Custom calldata for contract interactions on destination chain * **destinationCallValue** (number): Value (in wei) to send with the destination call * **fundMethod** (FundMethod): Hint for the funding method — `WALLET`, `DIRECT_TRANSFER`, `ONRAMP_MESH`, or `ONRAMP_MELD` * **onlyNativeGasFee** (boolean): When `true`, restricts gas fee options to the origin chain's native token only * **options** (QuoteIntentRequestOptions): * **bridgeProvider** (RouteProvider): Preferred bridge provider — `AUTO` (default), `RELAY`, `CCTP`, `LZ_OFT`, `SUSHI`, `ZEROX`, `LIFI`, `WETH` * **bridgeProviderFallback** (boolean): If `true`, fall back to another provider if the preferred bridge provider is unavailable * **swapProvider** (RouteProvider): Preferred same-chain swap provider — `AUTO` (default), `RELAY`, `SUSHI`, `ZEROX` * **swapProviderFallback** (boolean): If `true`, fall back to another provider if the preferred swap provider is unavailable * **preference** (RoutePreference): Route optimisation target — `RECOMMENDED` (default), `FASTEST`, `CHEAPEST`, `TRUSTLESS` * **intentProtocol** (IntentProtocolVersion): Force a specific intent protocol version — `v1` or `v1_5` * **slippageTolerance** (number): Maximum acceptable slippage as a fraction of 1 (e.g. `0.005` for 0.5%) * **trailsAddressOverrides** (TrailsAddressOverrides): Custom Sequence wallet addresses for the intent ## Response The response includes: * **intent** (Intent): Complete intent object with transaction details * **gasFeeOptions** (GasFeeOptions): Available gas fee payment options ### Intent Object Details The intent object contains: * Unique intent ID * Transaction status * Deposit transaction details * Cross-chain calls to execute * Quote information (rates, slippage, price impact) * Fee breakdown (gas fees, provider fees) * Expiration timestamp ## Example ```typescript theme={null} const quoteRequest = { ownerAddress: "0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1", originChainId: 1, // Ethereum originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC originTokenAmount: 100000000, // 100 USDC (6 decimals) destinationChainId: 8453, // Base destinationTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base destinationToAddress: "0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1", tradeType: "EXACT_INPUT", options: { slippageTolerance: 0.005, // 0.5% bridgeProvider: "RELAY" } }; const response = 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(quoteRequest) }); const quote = await response.json(); console.log('Quote:', quote); ``` ## Quote Expiration Quotes expire **15 minutes** after being issued. You must call `ExecuteIntent` (or `ExecuteIntentEdge` for a Solana/Tron origin) before the quote expires, or you'll need to request a new quote. ## Next Steps After receiving a quote: 1. Review the quote details, fees, and estimated amounts. 2. Call [`ExecuteIntent`](/api-reference/endpoints/execute-intent) with the `intent.intentId` from the response to execute directly from `QUOTED`. For a Solana or Tron origin, call [`ExecuteIntentEdge`](/api-reference/endpoints/execute-intent-edge) first to arm fill monitoring, then have the user deposit on the edge chain. `CommitIntent` is deprecated for new integrations. Existing callers may still commit before executing; new integrations should skip it and call `ExecuteIntent` directly on the quoted intent. See [CommitIntent](/api-reference/endpoints/commit-intent) for details. # QuoteIntentEdge Source: https://docs.trails.build/api-reference/endpoints/quote-intent-edge trails-api.gen.json post /rpc/Trails/QuoteIntentEdge ## Overview The `QuoteIntentEdge` endpoint requests a quote for a cross-chain intent that uses a non-EVM **edge rail** (Solana or Tron) on either the origin or destination side. Edge rails extend the standard intent flow with an external bridge hop that hands funds off to (or receives funds from) the EVM intent wallet. Use `QuoteIntent` for EVM-only routes. Use `QuoteIntentEdge` whenever Solana or Tron is involved. ## Request Parameters ### Required Fields * **intent** (`QuoteIntentRequest`): The outer intent shape. The origin must be left unset — `intent.originChainId` must be `0` (or omitted) when `edge.mode = ORIGIN`. The `edge` request determines the actual origin chain/token at quote time. * **edge** (`EdgeQuoteRequest`): * **rail** (`EdgeRail`): `SOLANA` or `TRON`. * **mode** (`EdgeMode`): `ORIGIN` (edge funds the intent from a non-EVM chain) or `DESTINATION` (intent funds an address on a non-EVM chain). * **provider** (`RouteProvider`, optional): External bridge provider (defaults to AUTO selection). * **tradeType** (`TradeType`, optional): Controls the external funding rail. Defaults to `EXACT_OUTPUT` (funds the fixed handoff amount required by the quoted intent). `EXACT_INPUT` spends `originTokenAmount` on the origin rail and derives the intent from the rail's quoted minimum output. ### ORIGIN Mode Fields Required when `edge.mode = ORIGIN`: * **originChainId** (number): Non-EVM origin chain ID (Solana or Tron). * **originAddress** (string): User's address on the non-EVM chain. * **originTokenAddress** (string): Token address on the non-EVM chain. Optional for `ORIGIN`: * **originTokenAmount** (bigint): Required when `tradeType = EXACT_INPUT`. * **refundAddress** (string): Refund destination on the non-EVM chain. ### DESTINATION Mode Fields Required when `edge.mode = DESTINATION`: * **destinationChainId** (number): Non-EVM destination chain ID. * **destinationAddress** (string): Recipient address on the non-EVM chain. * **destinationTokenAddress** (string): Token address on the non-EVM chain. ## Response * **edgeId** (string): The Trails edge quote ID. Pass this to `GetEdgeStatus` to track the external rail leg. * **edge** (`EdgeQuoteResponse`): Quote details for the external rail, including the `handoff` describing the chain, token, amount, and recipient address used to hand funds off between the rail and the intent wallet, plus the provider-specific `providerQuote` and `executionPlan`. * **intent** (`Intent`, optional): The committed intent shape — same structure as `QuoteIntent` returns. * **gasFeeOptions** (`GasFeeOptions`, optional) * **transactionStates** (array, optional) * **passthrough** (`PassthroughInfo`, optional) ## Discovery Call [`GetEdges`](/api-reference/endpoints/get-edges) to find out which edge rails and modes are enabled on this deployment before quoting. ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/QuoteIntentEdge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intent: { ownerAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', originChainId: 0, // must be 0 in ORIGIN mode — set by edge destinationChainId: 8453, destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base destinationToAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', destinationTokenAmount: 100000000 }, edge: { rail: 'SOLANA', mode: 'ORIGIN', tradeType: 'EXACT_OUTPUT', originChainId: 1399811149, // Solana originAddress: 'AbC...', originTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC mint } }) }); const { edgeId, edge, intent } = await response.json(); ``` ## Next Steps Track the external rail leg's status List enabled edge rails and modes # RetryIntent Source: https://docs.trails.build/api-reference/endpoints/retry-intent trails-api.gen.json post /rpc/Trails/RetryIntent ## Overview The `RetryIntent` endpoint repairs a failed user-submitted deposit flow by validating a replacement deposit transaction hash, marking the deposit leg as confirmed, and requeueing downstream execution. ## Use Cases * Recover from a failed or dropped deposit transaction * Resubmit a deposit with a corrected transaction hash * Resume execution of an intent that failed during the deposit phase ## Request Parameters ### Required Fields * **intentId** (string): The unique ID of the intent to retry * **depositTransactionHash** (string): The transaction hash of the new or corrected deposit transaction ## Response * **intentId** (string): The ID of the retried intent * **intentStatus** (`IntentStatus`): Updated status of the intent after retry ## Examples ### Retry a Failed Deposit ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/RetryIntent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId: 'intent_abc123', depositTransactionHash: '0xnewdeposithash...' }) }); const { intentId, intentStatus } = await response.json(); console.log(`Intent ${intentId} is now ${intentStatus}`); ``` ### With the Trails SDK ```typescript theme={null} import { TrailsClient } from '0xtrails' const trails = new TrailsClient({ accessKey: 'YOUR_ACCESS_KEY' }) const result = await trails.retryIntent({ intentId: 'intent_abc123', depositTransactionHash: '0xnewdeposithash...' }) console.log('Retry result:', result.intentStatus) ``` The replacement deposit transaction must be confirmed on-chain before calling this endpoint. The `depositTransactionHash` must correspond to a valid on-chain transaction that deposits the correct token and amount to the intent address. ## Next Steps Check the receipt after retrying Stream updates until the intent completes # SearchIntents Source: https://docs.trails.build/api-reference/endpoints/search-intents trails-api.gen.json post /rpc/Trails/SearchIntents ## Overview The `SearchIntents` endpoint allows you to search for intents using various criteria such as owner address, intent address, or deposit transaction hash. This is useful for finding all intents associated with a wallet or tracking specific transactions. ## Use Cases * Find all intents for a specific wallet address * Look up an intent by its deposit transaction hash * Track intents associated with a specific intent contract address * Build transaction history interfaces * Audit and analytics * Retrieve intent IDs for status monitoring ## Request Parameters All parameters are optional, but at least one should be provided: * **byOwnerAddress** (string): Search by wallet address that created the intent * **byOriginIntentAddress** (string): Search by origin chain intent contract address * **byDepositTransactionHash** (string): Search by deposit transaction hash ## Response The response includes: * **intents** (Intent\[]): Array of matching intent objects Each intent in the array contains all the standard intent fields as described in the `GetIntent` endpoint. ## Examples ### Search by Owner Address Find all intents created by a specific wallet: ```typescript theme={null} const searchResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/SearchIntents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOwnerAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1' }) }); const { intents } = await searchResponse.json(); console.log(`Found ${intents.length} intents`); intents.forEach(intent => { console.log(`Intent ${intent.intentId}: ${intent.status}`); }); ``` ### Search by Deposit Transaction Hash Look up an intent using its deposit transaction: ```typescript theme={null} const searchResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/SearchIntents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byDepositTransactionHash: '0x1234567890abcdef...' }) }); const { intents } = await searchResponse.json(); if (intents.length > 0) { console.log('Found intent:', intents[0].intentId); console.log('Status:', intents[0].status); } else { console.log('No intent found for this transaction'); } ``` ### Search by Origin Intent Address Find all intents using a specific intent contract: ```typescript theme={null} const searchResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/SearchIntents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOriginIntentAddress: '0xabcdef1234567890...' }) }); const { intents } = await searchResponse.json(); console.log('Found intents:', intents.map(i => i.intentId)); ``` ## Building a Transaction History Create a user-friendly transaction history: ```typescript theme={null} async function getTransactionHistory(userAddress: string) { const { intents } = await fetch('https://trails-api.sequence.app/rpc/Trails/SearchIntents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOwnerAddress: userAddress }) }).then(r => r.json()); // Sort by creation date (newest first) const sorted = intents.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); // Format for display return sorted.map(intent => ({ id: intent.intentId, status: intent.status, fromChain: intent.quoteRequest.originChainId, toChain: intent.quoteRequest.destinationChainId, fromAmount: intent.quote.fromAmount, toAmount: intent.quote.toAmount, totalFees: intent.fees.totalFeeUsd, createdAt: intent.createdAt, expiresAt: intent.expiresAt })); } const history = await getTransactionHistory('0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1'); console.table(history); ``` ## UI Component Example ```tsx theme={null} import { useState, useEffect } from 'react'; import type { Intent } from '0xtrails'; function IntentHistory({ userAddress }: { userAddress: string }) { const [intents, setIntents] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { async function loadIntents() { try { const response = await fetch( 'https://trails-api.sequence.app/rpc/Trails/SearchIntents', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ byOwnerAddress: userAddress }) } ); const { intents } = await response.json(); setIntents(intents); } catch (error) { console.error('Failed to load intents:', error); } finally { setLoading(false); } } loadIntents(); }, [userAddress]); if (loading) return
Loading...
; return (

Transaction History

{intents.length === 0 ? (

No transactions found

) : (
    {intents.map(intent => (
  • {intent.intentId} {intent.status}
    Chain {intent.quoteRequest.originChainId} → Chain {intent.quoteRequest.destinationChainId}
    {intent.quote.fromAmount} → {intent.quote.toAmount}
    Fee: ${intent.fees.totalFeeUsd}
    {new Date(intent.createdAt).toLocaleString()}
  • ))}
)}
); } ``` ## Filtering and Analytics ```typescript theme={null} import { TrailsApi, type Intent } from '@0xtrails/api' const trailsApi = new TrailsApi('YOUR_API_KEY') async function analyzeUserActivity(userAddress: string) { const { intents } = await trailsApi.searchIntents({ byOwnerAddress: userAddress }); const stats = { total: intents.length, succeeded: intents.filter((i: Intent) => i.status === 'SUCCEEDED').length, failed: intents.filter((i: Intent) => i.status === 'FAILED').length, pending: intents.filter((i: Intent) => ['QUOTED', 'COMMITTED', 'EXECUTING'].includes(i.status) ).length, totalVolume: intents.reduce((sum: number, i: Intent) => sum + Number(i.quote.fromAmount), 0), totalFees: intents.reduce((sum: number, i: Intent) => sum + (i.fees.totalFeeUsd), 0), chains: [...new Set(intents.flatMap((i: Intent) => [i.quoteRequest.originChainId, i.quoteRequest.destinationChainId] ))], providers: [...new Set(intents.map((i: Intent) => i.quote.quoteProvider))] }; return stats; } const stats = await analyzeUserActivity('0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1'); console.log('User Activity:', stats); ``` ## Pagination Considerations The `SearchIntents` endpoint may return a large number of results for active addresses. Consider implementing client-side pagination or using `GetIntentHistory` for paginated results. ## Best Practices 1. **Cache results**: Store search results locally to reduce API calls 2. **Filter on client**: Apply additional filters on the client side after fetching 3. **Limit date range**: Use `createdAt` timestamps to filter by date range 4. **Combine with GetIntentReceipt**: Fetch receipts for intents that need transaction hashes ## Example: Recent Active Intents ```typescript theme={null} import { TrailsApi, type Intent } from '@0xtrails/api' const trailsApi = new TrailsApi('YOUR_API_KEY') async function getRecentActiveIntents(userAddress: string, hoursAgo: number = 24) { const { intents } = await trailsApi.searchIntents({ byOwnerAddress: userAddress }); const cutoff = Date.now() - (hoursAgo * 60 * 60 * 1000); return intents .filter((i: Intent) => i.createdAt && new Date(i.createdAt).getTime() > cutoff) .filter((i: Intent) => ['COMMITTED', 'EXECUTING'].includes(i.status)) .sort((a, b) => new Date(b.createdAt ?? '').getTime() - new Date(a.createdAt ?? '').getTime() ); } const activeIntents = await getRecentActiveIntents(userAddress, 24); console.log('Active intents in last 24h:', activeIntents.length); ``` For paginated transaction history with full receipt data, use the `GetIntentHistory` endpoint instead. ## Next Steps * Use `GetIntent` to get full details for specific intents * Use `GetIntentReceipt` to get transaction hashes and status * Use `GetIntentHistory` for paginated history views with receipts # WaitIntentReceipt Source: https://docs.trails.build/api-reference/endpoints/wait-intent-receipt trails-api.gen.json post /rpc/Trails/WaitIntentReceipt ## Overview The `WaitIntentReceipt` endpoint provides a streaming or long-polling mechanism to wait for an intent to complete. Unlike `GetIntentReceipt`, which requires manual polling, this endpoint holds the connection until the intent reaches a terminal state or times out. ## Use Cases * Wait for transaction completion without polling * Implement real-time transaction monitoring * Reduce API calls by using long-polling * Get notified immediately when transaction completes * Simplify client-side code for waiting ## Request Parameters ### Required Fields * **intentId** (string): The unique identifier of the intent to monitor ## Response The response includes: * **intentReceipt** (IntentReceipt): Complete receipt with transaction details * **done** (boolean): Indicates if the intent has reached a terminal state * `true`: Intent is complete (SUCCEEDED or FAILED) * `false`: Intent is still processing (may require another call) ## Behavior This endpoint will: 1. Check the current status of the intent 2. If complete (`SUCCEEDED` or `FAILED`), return immediately with `done: true` 3. If still processing, wait for state changes 4. Return updates as they occur 5. Timeout after a reasonable period if no completion ## Example ```typescript theme={null} async function waitForCompletion(intentId: string) { const response = await fetch('https://trails-api.sequence.app/rpc/Trails/WaitIntentReceipt', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId }) }); const { intentReceipt, done } = await response.json(); if (done) { if (intentReceipt.status === 'SUCCEEDED') { console.log('Transaction succeeded!'); return intentReceipt; } else { throw new Error(`Transaction failed: ${intentReceipt.originTransaction.statusReason}`); } } else { // Not done yet, call again return waitForCompletion(intentId); } } // Usage try { const receipt = await waitForCompletion('intent_123abc'); console.log('Completed:', receipt); } catch (error) { console.error('Failed:', error); } ``` ## Comparison: WaitIntentReceipt vs GetIntentReceipt ### GetIntentReceipt (Manual Polling) ```typescript theme={null} // Requires manual polling loop async function pollReceipt(intentId: string) { while (true) { const { intentReceipt } = await getIntentReceipt(intentId); if (intentReceipt.status === 'SUCCEEDED' || intentReceipt.status === 'FAILED') { return intentReceipt; } await new Promise(resolve => setTimeout(resolve, 2000)); // 2s delay } } ``` ### WaitIntentReceipt (Long Polling) ```typescript theme={null} // Automatic waiting, no manual delay needed async function waitReceipt(intentId: string) { const { intentReceipt, done } = await waitIntentReceipt(intentId); if (!done) { return waitReceipt(intentId); // Recursively wait if needed } return intentReceipt; } ``` `WaitIntentReceipt` is more efficient as it reduces the number of API calls and provides near-instant updates. ## With Progress Updates ```typescript theme={null} async function waitWithProgress(intentId: string, onProgress?: (status: string) => void) { let lastStatus = ''; const wait = async (): Promise => { const { intentReceipt, done } = await fetch( 'https://trails-api.sequence.app/rpc/Trails/WaitIntentReceipt', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId }) } ).then(r => r.json()); // Report progress if status changed if (intentReceipt.status !== lastStatus) { lastStatus = intentReceipt.status; onProgress?.(intentReceipt.status); } if (done) { return intentReceipt; } return wait(); // Continue waiting }; return wait(); } // Usage with progress callback const receipt = await waitWithProgress('intent_123abc', (status) => { console.log('Status update:', status); // Update UI, show loading state, etc. }); ``` ## Timeout Handling ```typescript theme={null} async function waitWithTimeout(intentId: string, timeoutMs = 300000) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Timeout waiting for intent')), timeoutMs); }); const waitPromise = (async () => { let done = false; let receipt; while (!done) { const response = await fetch( 'https://trails-api.sequence.app/rpc/Trails/WaitIntentReceipt', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId }) } ); const data = await response.json(); receipt = data.intentReceipt; done = data.done; } return receipt; })(); return Promise.race([waitPromise, timeoutPromise]); } try { const receipt = await waitWithTimeout('intent_123abc', 60000); // 1 minute timeout console.log('Completed:', receipt); } catch (error) { console.error('Timed out or failed:', error); } ``` ## Best Practices Always implement timeout handling to prevent indefinite waiting in case of network issues or unexpected states. For UI applications, combine this endpoint with progress callbacks to provide real-time feedback to users. ## Next Steps After receiving the completed receipt: 1. Verify transaction hashes on block explorers 2. Update UI to show completion status 3. Trigger any post-transaction logic 4. Store receipt for record-keeping # YieldCreateEnterAction Source: https://docs.trails.build/api-reference/endpoints/yield-create-enter-action trails-api.gen.json post /rpc/Trails/YieldCreateEnterAction Generate unsigned deposit transaction calldata for a DeFi yield market ## Overview `YieldCreateEnterAction` returns the unsigned transaction payload needed to deposit into a DeFi yield market. This is the low-level endpoint that backs the `lend` and `deposit` composable actions in the SDK. Use this endpoint when building custom DeFi UIs outside React, or when you need to construct the calldata manually before passing it to Trails via the `to.calldata` param on `QuoteIntent`. For React apps, use the `lend()` or `deposit()` action builders with `useTrailsSendTransaction` — they call this endpoint internally. ## Request Parameters | Field | Type | Required | Description | | - | - | - | - | | `earnMarketId` | string | Yes | Market ID from `YieldGetMarkets` | | `userWalletAddress` | string | Yes | Address of the depositing wallet | | `args` | object | No | Market-specific arguments (varies by protocol) | ## Response Returns `payload` containing unsigned transaction data: | Field | Description | | - | - | | `transactions` | Array of transaction objects to execute | | `transactions[].to` | Contract address | | `transactions[].data` | ABI-encoded calldata | | `transactions[].value` | ETH value (for native token deposits) | ## Examples ### Build a deposit calldata for Aave ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldCreateEnterAction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ earnMarketId: 'base-usdc-aave-v3-lending', userWalletAddress: '0xYourWalletAddress', }), }) const { payload } = await response.json() const action = JSON.parse(payload) // Pass action.transactions[0].data to QuoteIntent as to.calldata ``` ### With protocol-specific arguments Some markets accept additional arguments (e.g. slippage, referral codes): ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldCreateEnterAction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ earnMarketId: 'ethereum-usdc-morpho-0x...', userWalletAddress: '0xYourWalletAddress', args: { referralCode: 0, }, }), }) ``` ## SDK alternative For React, use the `lend` or `deposit` builders with `useTrailsSendTransaction`: ```tsx theme={null} import { useTrailsSendTransaction, lend, deposit } from '0xtrails' const { sendTransaction } = useTrailsSendTransaction({ actions: [ lend({ marketId: 'base-usdc-aave-v3-lending', amount: '100' }), ], }) ``` See [Composable Actions](/use-cases/composable-actions) for the full SDK approach. ## See also * [YieldCreateExitAction](/api-reference/endpoints/yield-create-exit-action) — Withdraw from a position * [YieldGetMarkets](/api-reference/endpoints/yield-get-markets) — Discover market IDs * [Composable Actions](/use-cases/composable-actions) — SDK builders for DeFi deposits # YieldCreateExitAction Source: https://docs.trails.build/api-reference/endpoints/yield-create-exit-action trails-api.gen.json post /rpc/Trails/YieldCreateExitAction Generate unsigned withdrawal transaction calldata for a DeFi yield position ## Overview `YieldCreateExitAction` returns the unsigned transaction payload needed to withdraw from a DeFi yield position. It is the counterpart to `YieldCreateEnterAction`. Use this endpoint to build withdrawal UIs or construct exit calldata for Trails routes. For React apps, the composable action builders handle this automatically. ## Request Parameters | Field | Type | Required | Description | | - | - | - | - | | `earnMarketId` | string | Yes | Market ID from `YieldGetMarkets` | | `userWalletAddress` | string | Yes | Address of the withdrawing wallet | | `args` | object | No | Market-specific arguments (e.g. amount, shares) | ## Response Returns `payload` containing unsigned transaction data: | Field | Description | | - | - | | `transactions` | Array of transaction objects to execute | | `transactions[].to` | Contract address | | `transactions[].data` | ABI-encoded calldata | | `transactions[].value` | ETH value (for native token withdrawals) | ## Examples ### Withdraw from an Aave lending position ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldCreateExitAction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ earnMarketId: 'base-usdc-aave-v3-lending', userWalletAddress: '0xYourWalletAddress', }), }) const { payload } = await response.json() const action = JSON.parse(payload) ``` ### Withdraw a specific amount Use `args` to pass withdrawal parameters for markets that require them (e.g. partial withdrawals): ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldCreateExitAction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ earnMarketId: 'ethereum-usdc-morpho-0x...', userWalletAddress: '0xYourWalletAddress', args: { amount: '1000000', // 1 USDC in wei }, }), }) ``` ## Common pattern: check balance then exit ```typescript theme={null} // 1. Check what positions the wallet holds const balancesRes = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetAggregateBalances', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ queries: [{ address: '0xYourWallet', network: 'base' }] }), }) const { payload: balPayload } = await balancesRes.json() const balances = JSON.parse(balPayload) // 2. Build exit calldata for each position for (const position of balances) { const exitRes = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldCreateExitAction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ earnMarketId: position.yieldId, userWalletAddress: '0xYourWallet', }), }) const { payload: exitPayload } = await exitRes.json() const exitAction = JSON.parse(exitPayload) // Use exitAction.transactions to build and submit the withdrawal } ``` ## See also * [YieldCreateEnterAction](/api-reference/endpoints/yield-create-enter-action) — Deposit into a position * [YieldGetAggregateBalances](/api-reference/endpoints/yield-get-balances) — Check existing positions before exiting * [YieldGetMarkets](/api-reference/endpoints/yield-get-markets) — Discover market IDs # YieldGetAggregateBalances Source: https://docs.trails.build/api-reference/endpoints/yield-get-balances trails-api.gen.json post /rpc/Trails/YieldGetAggregateBalances Fetch a wallet's earn balances across chains and yield positions ## Overview `YieldGetAggregateBalances` returns a wallet's active yield positions across one or more chains. Use this to show users their current DeFi holdings — principal, earned yield, and which markets they are in. The SDK's `useEarnBalances` hook wraps this endpoint — prefer it in React apps. ## Request Parameters | Field | Type | Required | Description | | - | - | - | - | | `queries` | object\[] | Yes | Array of 1–25 balance queries | Each query object: | Field | Type | Required | Description | | - | - | - | - | | `address` | string | Yes | Wallet address to look up | | `network` | string | Yes | Network identifier (e.g. `"ethereum"`, `"base"`, `"polygon"`) | You can pass up to 25 queries per request, enabling multi-chain balance lookups in a single call. ## Response Returns `payload` containing balance records. Each balance entry includes: | Field | Description | | - | - | | `yieldId` | Market ID — matches the `id` field from `YieldGetMarkets` | | `address` | Wallet address | | `network` | Network the position is on | | `amount` | Balance amount in the market's token | | `amountUsd` | USD value of the position | ## Examples ### Fetch balances on a single chain ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetAggregateBalances', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ queries: [ { address: '0xYourWalletAddress', network: 'base', }, ], }), }) const { payload } = await response.json() const balances = JSON.parse(payload) balances.forEach(position => { console.log(`${position.yieldId}: ${position.amountUsd} USD`) }) ``` ### Multi-chain balance lookup ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetAggregateBalances', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ queries: [ { address: '0xYourWalletAddress', network: 'base' }, { address: '0xYourWalletAddress', network: 'ethereum' }, { address: '0xYourWalletAddress', network: 'polygon' }, ], }), }) const { payload } = await response.json() const balances = JSON.parse(payload) ``` ## SDK alternative In React, use the `useEarnBalances` hook: ```tsx theme={null} import { useEarnBalances } from '0xtrails' // Single chain const { data: balances } = useEarnBalances({ walletAddress: '0xYourWalletAddress', chain: 'base', }) // Multiple chains const { data: balances } = useEarnBalances({ walletAddress: '0xYourWalletAddress', chains: ['base', 'ethereum', 'polygon'], }) ``` ## See also * [YieldGetMarkets](/api-reference/endpoints/yield-get-markets) — Match `yieldId` against market IDs * [YieldCreateExitAction](/api-reference/endpoints/yield-create-exit-action) — Build withdrawal transactions for active positions * [Markets & Providers](/sdk/composable-actions/markets-and-providers) — SDK hooks reference # YieldGetMarketById Source: https://docs.trails.build/api-reference/endpoints/yield-get-market-by-id trails-api.gen.json post /rpc/Trails/YieldGetMarketById ## Overview The `YieldGetMarketById` endpoint returns a single yield market — including its tokens, reward rate, statistics, risk ratings, status, mechanics, and current state — by market ID. Use it to fetch full detail for a market discovered through [`YieldGetMarkets`](/api-reference/endpoints/yield-get-markets). ## Request Parameters ### Required Fields * **marketId** (string): The market ID returned by `YieldGetMarkets`. ## Response * **market** (`YieldMarket`): The full market object. Notable fields: * **id**, **network**, **chainId**, **providerId** * **inputTokens**, **outputToken**, **token**, **tokens** (`YieldToken`) * **rewardRate** (`YieldRewardRate`): includes `total`, `rateType`, and per-source components * **statistics** (`YieldStatistics`): `tvlUsd`, `tvl`, `uniqueUsers`, average position size, etc. * **risk** (`YieldRiskSummary`): array of `{rating, source}` ratings * **status** (`YieldStatus`): `{enter, exit}` booleans gating entry/exit * **mechanics** (`YieldMechanics`): type, reward schedule, gas fee token, lockup, cooldown, fees, entry limits, requirements * **state** (`YieldState`): price-per-share, concentrated-liquidity-pool state, capacity, liquidity, allocations * **supportsBeneficiary** (boolean) ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetMarketById', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ marketId: 'market_abc123' }) }); const { market } = await response.json(); console.log(market.providerId, market.rewardRate.total, market.statistics?.tvlUsd); ``` ## Next Steps Search and list markets Build a deposit action for a market # YieldGetMarkets Source: https://docs.trails.build/api-reference/endpoints/yield-get-markets trails-api.gen.json post /rpc/Trails/YieldGetMarkets List and filter DeFi markets available through the Trails earn system ## Overview `YieldGetMarkets` returns DeFi markets available for deposit through Trails, including lending markets (Aave, Morpho) and yield vaults (Yearn, ERC-4626). Use this to power market selection UIs or discover `marketId` values for composable actions. The SDK's `useEarnMarkets` hook wraps this endpoint with typed filters and caching — prefer it in React apps. ## Request Parameters All fields are optional. | Field | Type | Description | | - | - | - | | `provider` | string | Filter by protocol (e.g. `"aave"`, `"morpho"`, `"yearn"`) | | `chainId` | string | Filter by chain ID (e.g. `"8453"` for Base) | | `type` | string | Market category: `"lending"` or `"vault"` | | `search` | string | Free-text search over market names and tokens | | `sort` | string | Sort order (e.g. `"rewardRateDesc"`) | | `limit` | number | Number of results to return | | `offset` | number | Pagination offset | ## Response Returns `payload` containing an array of market objects. Each market includes: | Field | Description | | - | - | | `id` | Unique market ID — pass this to `lend()` or `deposit()` composable actions | | `providerId` | Protocol identifier (e.g. `"aave"`, `"morpho"`) | | `rewardRate` | Current APY as a decimal (e.g. `0.045` = 4.5%) | | `statistics.tvlUsd` | Total value locked in USD | | `metadata.name` | Human-readable market name | | `metadata.token` | Underlying token info (symbol, address, decimals) | ## Examples ### List all markets on Base ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetMarkets', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ chainId: '8453', limit: 20, offset: 0, }), }) const { payload } = await response.json() const markets = JSON.parse(payload) markets.forEach(market => { console.log(`${market.id}: ${(market.rewardRate * 100).toFixed(2)}% APY`) }) ``` ### Filter lending markets by protocol ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetMarkets', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ provider: 'aave', type: 'lending', sort: 'rewardRateDesc', }), }) const { payload } = await response.json() const markets = JSON.parse(payload) ``` ### Search for USDC markets ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetMarkets', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ search: 'usdc', sort: 'rewardRateDesc', limit: 10, }), }) ``` ## SDK alternative In React, use the typed `useEarnMarkets` hook instead of calling this endpoint directly: ```tsx theme={null} import { useEarnMarkets } from '0xtrails' const { data: markets, isLoading } = useEarnMarkets({ chain: 'base', type: 'lending', provider: 'aave', search: 'usdc', sortBy: 'rewardRateDesc', limit: 20, }) // markets[i].id → pass to lend() or deposit() actions ``` ## See also * [YieldGetProviders](/api-reference/endpoints/yield-get-providers) — List available protocols * [Markets & Providers](/sdk/composable-actions/markets-and-providers) — SDK hooks reference # YieldGetNetworks Source: https://docs.trails.build/api-reference/endpoints/yield-get-networks trails-api.gen.json post /rpc/Trails/YieldGetNetworks ## Overview The `YieldGetNetworks` endpoint returns the networks (chains) that yield providers can be queried on. Use it to populate chain pickers in earn UIs or to validate that a target chain is supported before calling [`YieldGetMarkets`](/api-reference/endpoints/yield-get-markets). ## Request `YieldGetNetworks` takes no parameters. ## Response * **networks** (array of `YieldNetwork`): * **id** (string) * **name** (string) * **category** (string) * **logoURI** (string) ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetNetworks', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({}) }); const { networks } = await response.json(); for (const network of networks) { console.log(network.id, network.name); } ``` ## Next Steps List markets on a network List yield providers # YieldGetProvider Source: https://docs.trails.build/api-reference/endpoints/yield-get-provider trails-api.gen.json post /rpc/Trails/YieldGetProvider ## Overview The `YieldGetProvider` endpoint returns a single yield provider by its ID. Use it to fetch full provider metadata (logo, description, website, TVL, supported standards, beneficiary support) when you already know the provider ID — for example, when rendering details for a market returned from [`YieldGetMarkets`](/api-reference/endpoints/yield-get-markets). ## Request Parameters ### Required Fields * **providerId** (string): The yield provider ID (for example `"aave"`, `"morpho"`, `"yearn"`). ## Response * **provider** (`YieldProvider`): The full provider object including `name`, `id`, `logoURI`, `description`, `website`, `tvlUsd`, `type`, `references`, and `supportsBeneficiary`. ## Example ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetProvider', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ providerId: 'aave' }) }); const { provider } = await response.json(); console.log(provider.name, provider.tvlUsd); ``` ## Next Steps List all yield providers List markets, optionally filtered by provider # YieldGetProviders Source: https://docs.trails.build/api-reference/endpoints/yield-get-providers trails-api.gen.json post /rpc/Trails/YieldGetProviders List DeFi protocols available through the Trails earn system ## Overview `YieldGetProviders` returns the DeFi protocols (providers) available through Trails' earn system — Aave, Morpho, Yearn, and others. Use the returned provider IDs as filter values for `YieldGetMarkets`. The SDK's `useEarnProviders` hook wraps this endpoint — prefer it in React apps. ## Request Parameters All fields are optional. | Field | Type | Description | | - | - | - | | `limit` | number | Number of providers to return | | `offset` | number | Pagination offset | ## Response Returns `payload` containing an array of provider objects. Each provider includes: | Field | Description | | - | - | | `id` | Provider identifier — use as the `provider` filter in `YieldGetMarkets` | | `name` | Human-readable protocol name | | `url` | Protocol website | | `logoUrl` | Protocol logo image URL | ## Examples ### List all providers ```typescript theme={null} const response = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldGetProviders', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({}), }) const { payload } = await response.json() const providers = JSON.parse(payload) providers.forEach(provider => { console.log(`${provider.id}: ${provider.name}`) }) ``` ## SDK alternative In React, use the `useEarnProviders` hook: ```tsx theme={null} import { useEarnProviders } from '0xtrails' const { data: providers } = useEarnProviders() ``` ## See also * [YieldGetMarkets](/api-reference/endpoints/yield-get-markets) — Filter markets by provider ID * [Markets & Providers](/sdk/composable-actions/markets-and-providers) — SDK hooks reference # YieldSubmitTransactionHash Source: https://docs.trails.build/api-reference/endpoints/yield-submit-transaction-hash trails-api.gen.json post /rpc/Trails/YieldSubmitTransactionHash Report the on-chain hash of a broadcast Yield deposit or withdrawal transaction ## Overview `YieldSubmitTransactionHash` reports the on-chain transaction hash of a broadcast Yield action back to Trails' upstream Yield provider so it can advance lifecycle tracking and discover the resulting position in aggregate balance queries. Yield actions are constructed through [`YieldCreateEnterAction`](/api-reference/endpoints/yield-create-enter-action) and [`YieldCreateExitAction`](/api-reference/endpoints/yield-create-exit-action), then signed and broadcast by the client. This endpoint is how the client hands the resulting hash back to Trails so the position shows up in [`YieldGetBalances`](/api-reference/endpoints/yield-get-balances). ## Request Parameters | Field | Type | Required | Description | | - | - | - | - | | `transactionId` | string | Yes | Yield transaction ID returned in the `YieldAction` payload from `YieldCreateEnterAction` / `YieldCreateExitAction` | | `transactionHash` | string | Yes | On-chain hash of the broadcast transaction | Both fields are trimmed and rejected as `InvalidArgument` if empty. ## Response * **transaction** (`YieldTransaction`): The updated Yield transaction record. ## Example ```typescript theme={null} // Step 1: Build the deposit action const actionResponse = await fetch('https://trails-api.sequence.app/rpc/Trails/YieldCreateEnterAction', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ earnMarketId: 'base-usdc-aave-v3-lending', userWalletAddress: '0xYourWalletAddress', }), }) const { action } = await actionResponse.json() const { transactions } = JSON.parse(action.payload) // Step 2: Sign and broadcast on-chain const hash = await walletClient.sendTransaction({ to: transactions[0].to, data: transactions[0].data, value: BigInt(transactions[0].value ?? 0), }) // Step 3: Report the broadcast hash back to Yield await fetch('https://trails-api.sequence.app/rpc/Trails/YieldSubmitTransactionHash', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY', }, body: JSON.stringify({ transactionId: action.transactionId, transactionHash: hash, }), }) ``` ## Errors | Error | When | | - | - | | `InvalidArgument` | `transactionId` or `transactionHash` is missing/empty | | `Unavailable` | The Yield provider is not configured on this deployment | ## See also * [YieldCreateEnterAction](/api-reference/endpoints/yield-create-enter-action) — Build a deposit action * [YieldCreateExitAction](/api-reference/endpoints/yield-create-exit-action) — Build a withdrawal action * [YieldGetBalances](/api-reference/endpoints/yield-get-balances) — Read aggregate Yield balances # Getting Started Source: https://docs.trails.build/api-reference/introduction Learn how to integrate the Trails API for cross-chain transactions ## Welcome to the Trails API The Trails API enables seamless cross-chain token swaps, deposits, payments, and smart contract executions in a simplified interface with the Trails protocol. ## Authentication All API requests require an API key passed in the `X-Access-Key` header. ```typescript theme={null} const response = 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({ /* request body */ }) }); ``` **Get an API key**: Sign up and create an API key on the [Trails Dashboard](https://dashboard.trails.build/landing) ## Core Workflow Every interaction through Trails follows this flow: ```mermaid theme={null} flowchart LR A[Request Quote] --> B[Commit Quote] B --> C[Execute Quote] C --> D[Monitor Intent] ``` ### Get Wallet Balance Before requesting a quote, fetch the user's token balances using a multichain Indexer to display total available tokens and amounts for the user to select: ```typescript theme={null} const balancesResponse = await fetch('https://indexer.sequence.app/rpc/IndexerGateway/GetTokenBalances', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ accountAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', includeMetadata: true }) }); const { balances } = await balancesResponse.json(); // Display balances in your UI for user to select tokens, amount, and routes. console.log('Available tokens:', balances); ``` ### Get a Quote Request a quote to see rates, fees, and routing options for your transaction. ```typescript theme={null} 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: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', // sender address originChainId: 42161, // Arbitrum One originTokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC originTokenAmount: 100000000, // 100 USDC (6 decimals) destinationChainId: 8453, // Base destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base destinationToAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', // recipient tradeType: 'EXACT_INPUT', options: { slippageTolerance: 0.005, // 0.5% bridgeProvider: 'RELAY' } }) }); const { intent, gasFeeOptions } = await quoteResponse.json(); console.log('Quote received:', { fromAmount: intent.quote.fromAmount, toAmount: intent.quote.toAmount, totalFees: intent.fees.totalFeeUsd, expiresAt: intent.expiresAt }); ``` **Key Parameters:** * **ownerAddress**: User's wallet address * **originChainId** & **destinationChainId**: Source and destination chains * **originTokenAddress** & **destinationTokenAddress**: Token contracts * **originTokenAmount**: Amount to swap (in token's smallest unit) * **tradeType**: `EXACT_INPUT` (specify input) or `EXACT_OUTPUT` (specify output) Use `EXACT_INPUT` when you know how much you want to spend, and `EXACT_OUTPUT` when you know exactly how much you need to receive. ### (Legacy) Commit the Intent `CommitIntent` is deprecated for new integrations. New integrations skip this step and call `ExecuteIntent` directly on the quoted intent — `ExecuteIntent` accepts `QUOTED` intents and folds in the same validation `CommitIntent` used to perform. Existing callers may still commit before executing. The `intent` object contains everything needed to execute the intent and the server rejects mutated payloads, so pass it back unchanged. Quotes expire **15 minutes** after being issued. Call `ExecuteIntent` (or `ExecuteIntentEdge` for Solana/Tron origins) within this window, or request a new quote. Committing first does not extend the window; legacy callers still share the same 15-minute quote expiry. ### Execute the Transaction Execute the intent using one of two mutually exclusive methods - either a normal transfer to the intent address or a permit operation if a user wants to pay in a non-native gas token: Send tokens to the intent deposit address, then call `ExecuteIntent` with the transaction hash: ```typescript theme={null} import { createWalletClient, createPublicClient, http, encodeFunctionData, erc20Abi } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { base, arbitrum, mainnet } from 'viem/chains'; // Create clients (in React, you can also use the SDK's usePublicClient hook, or // useWalletClient from wagmi when paired with @0xtrails/adapter-wagmi) const walletClient = createWalletClient({ account: privateKeyToAccount('0x...'), chain: arbitrum, // Set to the origin chain transport: http() }); const publicClient = createPublicClient({ chain: arbitrum, // Set to the origin chain transport: http() }); ``` ```typescript theme={null} // Send tokens to the intent address (where Trails will execute from) const intentAddress = intent.depositTransaction.toAddress; const tokenAddress = intent.depositTransaction.tokenAddress; const amount = intent.depositTransaction.amount; const depositTxHash = await walletClient.sendTransaction({ to: tokenAddress, data: encodeFunctionData( { abi: erc20Abi, functionName: 'transfer', args: [intentAddress, amount] } ) }); // Wait for transaction confirmation using viem const receipt = await publicClient.waitForTransactionReceipt({ hash: depositTxHash }); // Then execute the intent with the deposit transaction hash and intentId 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: depositTxHash }) }); const { intentStatus } = await executeResponse.json(); console.log('Execution started:', intentStatus); ``` Sign ERC712 permit signatures which enables paying with any non-native gas tokens that are permit compatible: ```typescript theme={null} import { createWalletClient, createPublicClient, http, encodeFunctionData, erc20Abi, parseAbi } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { base, arbitrum, mainnet } from 'viem/chains'; // Create clients (in React, you can also use the SDK's usePublicClient hook, or // useWalletClient from wagmi when paired with @0xtrails/adapter-wagmi) const walletClient = createWalletClient({ account: privateKeyToAccount('0x...'), chain: arbitrum, // Set to the origin chain transport: http() }); const publicClient = createPublicClient({ chain: arbitrum, // Set to the origin chain transport: http() }); ``` ```typescript theme={null} // Select a gas fee option from the quote response const selectedGasFeeOption = gasFeeOptions.feeOptions[0]; // Choose based on user preference // Constants for the flow const INTENT_ENTRYPOINT_ADDRESS = '0x9470d883bac170116d397db3da71b2e57d567583'; // Trails Intent Entrypoint (same on all chains) const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now // 1. Get user nonce from Intent Entrypoint contract const userNonce = await publicClient.readContract({ address: INTENT_ENTRYPOINT_ADDRESS, abi: parseAbi([ 'function nonces(address user) view returns (uint256)' ]), functionName: 'nonces', args: [intent.ownerAddress] }); // 2. Check if approval is needed for the deposit token const tokenAddress = intent.depositTransaction.tokenAddress; const currentAllowance = await publicClient.readContract({ address: tokenAddress, abi: erc20Abi, functionName: 'allowance', args: [intent.ownerAddress, INTENT_ENTRYPOINT_ADDRESS] }); const needsApproval = currentAllowance < BigInt(intent.depositTransaction.amount); // 3. If approval needed, get permit signature for the deposit token let permitSignature; let permitDeadline; let permitAmount; if (needsApproval) { // Read token metadata for permit signature const tokenNonce = await publicClient.readContract({ address: tokenAddress, abi: parseAbi([ 'function nonces(address owner) view returns (uint256)' ]), functionName: 'nonces', args: [intent.ownerAddress] }); const tokenName = await publicClient.readContract({ address: tokenAddress, abi: parseAbi([ 'function name() view returns (string)' ]), functionName: 'name' }); permitDeadline = deadline; permitAmount = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; // maxUint256 for infinite approval permitSignature = await walletClient.signTypedData({ domain: { name: tokenName, version: '1', chainId: intent.originChainId, verifyingContract: tokenAddress }, types: { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' } ] }, primaryType: 'Permit', message: { owner: intent.ownerAddress, spender: INTENT_ENTRYPOINT_ADDRESS, value: BigInt(permitAmount), nonce: tokenNonce, deadline: BigInt(permitDeadline) } }); } // 4. Sign the intent signature (ERC712) with fee payment authorization const intentAddress = intent.depositTransaction.toAddress; const intentSignature = await walletClient.signTypedData({ domain: { name: 'TrailsIntentEntrypoint', version: '1', chainId: intent.originChainId, verifyingContract: INTENT_ENTRYPOINT_ADDRESS }, types: { TrailsIntent: [ { name: 'user', type: 'address' }, { name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, { name: 'intentAddress', type: 'address' }, { name: 'deadline', type: 'uint256' }, { name: 'chainId', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'feeAmount', type: 'uint256' }, { name: 'feeCollector', type: 'address' } ] }, primaryType: 'TrailsIntent', message: { user: intent.ownerAddress, token: tokenAddress, amount: intent.depositTransaction.amount, intentAddress: intentAddress, deadline: BigInt(deadline), chainId: BigInt(intent.originChainId), nonce: userNonce, feeAmount: BigInt(selectedGasFeeOption.amount), feeCollector: selectedGasFeeOption.feeCollectorAddress } }); // 5. Execute with signatures 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, depositSignature: { intentSignature, // ERC712 intent signature permitSignature, // ERC2612 permit signature (optional if already approved) permitDeadline, // Permit expiration timestamp (optional) permitAmount, // Permit amount - typically maxUint256 (optional) selectedGasFeeOption, // Fee option with: amount, tokenAddress, feeCollectorAddress userNonce: Number(userNonce), // User's current nonce from Intent Entrypoint deadline // Intent expiration timestamp } }) }); const { intentStatus } = await executeResponse.json(); console.log('Execution started:', intentStatus); ``` *** * Tokens must support ERC2612 permit (have `permit()` function) * User nonce is read from the Intent Entrypoint contract, not the quote response * Intent signature domain uses `TrailsIntentEntrypoint` name and the entrypoint contract as verifying contract **Two Scenarios:** 1. **First time / No approval**: Include `permitSignature`, `permitDeadline`, `permitAmount` 2. **Already approved**: Omit permit fields entirely for a 1-click experience (like the example above - only `intentSignature`, `selectedGasFeeOption`, `userNonce`, `deadline`) **Example Request When Already Approved:** ```json theme={null} { "intentId": "0xc467d1ebb4c6aab9efd3485d2627e51fb8ce1f662808eccf1222f364a1aa2d01", "depositSignature": { "intentSignature": "0xe3e5a87b8c1a9f882f6b251ea551dc1703ce617a0308418...", "selectedGasFeeOption": { "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "tokenSymbol": "USDC", "tokenDecimals": 6, "amount": "7845", "amountUsd": 0.007843596906252802, "feeCollectorAddress": "0x76008498f26789dd8b691bebe24c889a3dd1a2fc" }, "userNonce": 1, "deadline": 1762424131 } } ``` ### Monitor Completion Wait for the transaction to complete using the streaming endpoint: ```typescript theme={null} async function waitForCompletion(intentId: string) { while (true) { const waitResponse = await fetch( 'https://trails-api.sequence.app/rpc/Trails/WaitIntentReceipt', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': 'YOUR_ACCESS_KEY' }, body: JSON.stringify({ intentId }) } ); const { intentReceipt, done } = await waitResponse.json(); console.log('Status:', intentReceipt.status); // If done is true, the intent has reached a terminal state if (done) { if (intentReceipt.status === 'SUCCEEDED') { console.log('✅ Transaction completed!'); console.log('Deposit TX:', intentReceipt.depositTransaction.txnHash); console.log('Origin TX:', intentReceipt.originTransaction.txnHash); console.log('Destination TX:', intentReceipt.destinationTransaction.txnHash); return intentReceipt; } else { throw new Error('Transaction failed: ' + intentReceipt.originTransaction.statusReason); } } // If not done, the endpoint will have waited internally before returning until completion } } const receipt = await waitForCompletion(intentId); ``` Now you have an end to end API integration with Trails for seamless chain abstraction flows! You can continue this initial integration for specific use cases such as: * x402 Payments * AI Agents * Server-side currency conversion & settlement * Fund, Swap, Earn, or Pay for any application * Pass in optional calldata to call any smart contract function ## Error Handling The API returns errors in a consistent JSON format: ```json theme={null} { "webrpcError": { "code": 2000, "name": "InvalidArgument", "message": "Invalid token address", "cause": "originTokenAddress must be a valid EVM address", "httpStatus": 400 } } ``` ### Error Codes | Code | Name | Description | HTTP Status | | - | - | - | - | | 2000 | `InvalidArgument` | Request parameter is invalid or malformed | 400 | | 2001 | `Unexpected` | Unexpected server error | 500 | | 2002 | `Unavailable` | Requested resource is unavailable | 400 | | 2003 | `QueryFailed` | Database or external query failed | 400 | | 2004 | `IntentStatus` | Intent is in an invalid state for this operation | 422 | | 7000 | `IntentsSkipped` | Transaction doesn't require an intent (same chain/token) | 400 | | 7001 | `QuoteExpired` | Quote has expired, request a new one | 400 | | 7002 | `HighPriceImpact` | Quote unavailable due to high price impact | 422 | | 8000 | `NotFound` | Resource not found | 400 | | 8008 | `UnsupportedNetwork` | Chain ID is not supported | 422 | | 8009 | `ClientOutdated` | SDK/client version is outdated | 422 | | 9000 | `IntentsDisabled` | Intents service is temporarily unavailable | 400 | | 9001 | `ChainNodeHealth` | Quote unavailable due to interrupted chain node access | 503 | ### Handling Errors ```typescript theme={null} async function executeWithRetry(intentId: string, depositTxHash: string) { try { const response = await fetch('https://trails-api.sequence.app/rpc/Trails/ExecuteIntent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Key': process.env.TRAILS_API_KEY }, body: JSON.stringify({ intentId, depositTransactionHash: depositTxHash }) }); const data = await response.json(); if (data.webrpcError) { const { code, message, cause } = data.webrpcError; switch (code) { case 7001: // QuoteExpired // Request a new quote and retry throw new Error('Quote expired - request a new quote'); case 2004: // IntentStatus // Intent already executed or in wrong state console.log('Intent already processed'); break; case 9000: // IntentsDisabled // Service unavailable - retry with backoff await delay(5000); return executeWithRetry(intentId, depositTxHash); default: throw new Error(`API Error ${code}: ${message}. ${cause || ''}`); } } return data; } catch (error) { throw error; } } ``` ### Quote Expiration Quotes have strict time limits: | Stage | Time Limit | What Happens | | - | - | - | | Quote validity | 15 minutes | Quote expires, request a new one | | After ExecuteIntent / ExecuteIntentEdge | N/A | Trails handles execution timing | | Legacy: After CommitIntent | Shares quote expiry | Must call ExecuteIntent before the original 15-minute window closes | Always check quote expiration before executing. The `intent.expiresAt` timestamp is the single deadline for calling `ExecuteIntent` (or `ExecuteIntentEdge`), whether or not you committed first. ## Rate Limiting The API uses rate limiting to ensure service reliability. If you exceed limits, you'll receive HTTP 429 responses. | Limit Type | Value | Notes | | - | - | - | | Requests per second | 50 | Per API key | | Burst limit | 100 | Short burst allowance | | QuoteIntent calls | 20/min | Rate-limited separately | **Best practices:** * Cache `GetChains`, `GetTokenList` responses (they change infrequently) * Batch balance checks where possible * Implement exponential backoff on 429 responses ```typescript theme={null} async function fetchWithBackoff(url: string, options: RequestInit, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') || '1'); await delay(retryAfter * 1000 * Math.pow(2, i)); continue; } return response; } throw new Error('Max retries exceeded'); } ``` ## Next Steps Explore additional endpoints to enhance your integration: ### Transaction Management * **[GetIntent](/api-reference/endpoints/get-intent)** - Retrieve full intent details including quote, fees, and status * **[GetIntentReceipt](/api-reference/endpoints/get-intent-receipt)** - Poll for transaction status and get receipt with transaction hashes * **[WaitIntentReceipt](/api-reference/endpoints/wait-intent-receipt)** - Stream intent updates with automatic polling until completion * **[GetIntentHistory](/api-reference/endpoints/get-intent-history)** - Get complete transaction history with receipts for a user's wallet address ### Discovery & Search * **[SearchIntents](/api-reference/endpoints/search-intents)** - Search and filter intents by status, user, chain, or date range * **[GetTokenPrices](/api-reference/endpoints/get-token-prices)** - Get current USD prices for tokens to display values in your UI * **[GetChains](/api-reference/endpoints/get-chains)** - Get list of supported chains * **[GetTokenList](/api-reference/endpoints/get-token-list)** - Get available tokens for specified chains * **[GetExactInputRoutes](/api-reference/endpoints/get-exact-input-routes)** - Find destination tokens from a source token * **[GetExactOutputRoutes](/api-reference/endpoints/get-exact-output-routes)** - Find source tokens that route to a destination ### Earn * **[YieldGetMarkets](/api-reference/endpoints/yield-get-markets)** - List and filter DeFi markets (Aave, Morpho, Yearn) with APY and TVL * **[YieldGetProviders](/api-reference/endpoints/yield-get-providers)** - List available yield protocols * **[YieldGetAggregateBalances](/api-reference/endpoints/yield-get-balances)** - Fetch a wallet's active yield positions across chains * **[YieldCreateEnterAction](/api-reference/endpoints/yield-create-enter-action)** - Generate deposit calldata for a yield market * **[YieldCreateExitAction](/api-reference/endpoints/yield-create-exit-action)** - Generate withdrawal calldata from a yield position ## Support Need help? Join our community: * **Telegram**: [https://t.me/build\_with\_trails](https://t.me/build_with_trails) * **Documentation**: Browse the endpoint references for detailed information and advanced use cases * **Demo**: Try the [interactive playground](https://demo.trails.build/widget) # Trails API SDK Source: https://docs.trails.build/api-reference/trails-api-sdk TypeScript/JavaScript client for the Trails API ## Installation Install the Trails API client SDK: ```shell pnpm theme={null} pnpm install @0xtrails/api ``` ```shell npm theme={null} npm install @0xtrails/api ``` ```shell yarn theme={null} yarn add @0xtrails/api ``` ```shell bun theme={null} bun add @0xtrails/api ``` ## Quick Start Import and initialize the client with your API key: ```typescript theme={null} import { TrailsApi } from '@0xtrails/api' const trailsApi = new TrailsApi('YOUR_API_KEY') ``` **Get an API key**: Sign up and create an API key on the [Trails Dashboard](https://dashboard.trails.build/landing) ## Usage Example The SDK provides fully typed methods for all Trails API endpoints: ```typescript theme={null} import { TradeType, TrailsApi, RouteProvider } from '@0xtrails/api' // Request a quote const quoteResponse = await trailsApi.quoteIntent({ ownerAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', originChainId: 42161, // Arbitrum One originTokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC originTokenAmount: 100000000n, // 100 USDC destinationChainId: 8453, // Base destinationTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', destinationToAddress: '0x0709CF2d5D4f3D38f5948d697fE64d7FB3639Eb1', tradeType: TradeType.EXACT_INPUT, options: { slippageTolerance: 0.005, bridgeProvider: RouteProvider.RELAY } }) // Execute the intent directly from QUOTED — no separate commit step const { intent } = quoteResponse await trailsApi.executeIntent({ intentId: intent.intentId }) // Monitor completion const receipt = await trailsApi.waitIntentReceipt({ intentId: intent.intentId }) ``` For Solana or Tron origins, use `trailsApi.quoteIntentEdge()` and `trailsApi.executeIntentEdge()` instead. See [ExecuteIntentEdge](/api-reference/endpoints/execute-intent-edge). ## TypeScript Support The SDK is written in TypeScript and includes complete type definitions for all methods, request parameters, and response objects. You'll get full autocomplete and type checking in your IDE. ## API Reference All SDK methods correspond directly to the Trails API endpoints. For detailed information about request parameters, response formats, and advanced options, refer to the endpoint documentation: * [QuoteIntent](/api-reference/endpoints/quote-intent) - Get quotes for cross-chain transactions * [ExecuteIntent](/api-reference/endpoints/execute-intent) - Execute a quoted intent (EVM origin) * [ExecuteIntentEdge](/api-reference/endpoints/execute-intent-edge) - Arm a Solana or Tron origin edge intent for execution * [CommitIntent](/api-reference/endpoints/commit-intent) - Legacy commit step (deprecated for new integrations) * [WaitIntentReceipt](/api-reference/endpoints/wait-intent-receipt) - Monitor completion * [GetIntent](/api-reference/endpoints/get-intent) - Retrieve intent details * [GetIntentReceipt](/api-reference/endpoints/get-intent-receipt) - Get transaction receipt * [SearchIntents](/api-reference/endpoints/search-intents) - Search and filter intents * [GetIntentHistory](/api-reference/endpoints/get-intent-history) - Get transaction history with receipts * [GetTokenPrices](/api-reference/endpoints/get-token-prices) - Get current token prices # Pay Gas with Any Token Source: https://docs.trails.build/architecture/alt-fee-tokens Users can pay gas in any permit-compatible, non-native token ## Overview Trails supports paying gas fees in any permit-compatible (EIP-2612) token, including popular stablecoins like USDC and USDT. This removes the friction of needing native tokens on each chain. ## Why This Matters Common problems this solves: * **New chain onboarding**: Users bridge to a new chain but have no native tokens for gas * **Stablecoin-first users**: Users who hold primarily stablecoins don't need to manage gas tokens * **Simplified UX**: One token type for both payment and gas fees ## How It Works When a user initiates a transaction, the widget presents available fee tokens alongside the default native gas option. For permit-compatible tokens (EIP-2612), the user signs an off-chain permit rather than an on-chain approval — no separate gas-spending transaction is required. The Trails relayer uses that permit signature to cover the fee, deducting it from the user's selected token balance. ## Supported Fee Tokens Any token implementing EIP-2612 (permit) can be used as a fee token. See the [API Integration Guide](/api-reference/introduction) for the complete permit flow implementation at an API level. ## Widget Behavior In the Trails widget, alternative fee tokens are automatically presented to users when available. No additional configuration is required—the widget handles permit signatures and fee selection automatically. # Edge & Alternative VMs Source: https://docs.trails.build/architecture/edge-alt-vms How Trails extends beyond EVM to non-EVM chains via edge rails Trails routes across more than one virtual machine. Beyond EVM networks, it reaches **alternative VMs** — non-EVM ecosystems such as SVM chains — through a mechanism called **edge rails**. Every alternative-VM chain is an edge chain. This page explains the model. For the current list of supported edge chains, see [Supported Chains](/resources/supported-chains). For which SDK hooks are edge-aware, see [Hooks → Edge chains](/sdk/hooks#edge-chains). ## Edge rails An **edge rail** is the route that connects a non-EVM ecosystem to the EVM intent graph. Trails does not run a separate protocol per VM; instead, an edge chain sits at the *edge* of a flow — as the origin the user funds from, or the destination funds settle into — while the intent itself is anchored in the EVM-based orchestration described in [How Trails Works](/architecture/how-it-works). Because the model is keyed on the *rail*, not the specific chain, new alternative VMs are added by registering a new edge rail rather than by reworking the routing layer. ## Origin vs. destination Edge support is asymmetric. What an integration needs depends on whether the edge chain is the **origin** (where funds come from) or the **destination** (where funds land). | | Origin (pay *from* an edge chain) | Destination (pay *to* an edge chain) | | - | - | - | | **Wallet required** | An edge-chain wallet (non-EVM) | A standard EVM wallet | | **SDK adapter** | Required — the matching edge wallet [adapter](/sdk/adapters/overview) | Not required | | **What the user signs** | A transaction in their edge-chain wallet | Nothing on the edge chain | | **Recipient** | — | An edge-chain address | **Why origin needs an adapter.** A non-EVM chain uses its own signing scheme and transaction format — an EVM `walletClient` (viem) cannot produce a valid signature for it. Executing an edge-origin transaction therefore requires an ecosystem-native signer, supplied by the edge wallet adapter registered on `TrailsProvider` (currently the [Solana adapter](/sdk/adapters/solana)). Without it, edge-origin execution is unavailable and the SDK surfaces an explicit error. **Why destination does not.** When an edge chain is only the destination, the user still signs a single EVM origin transaction as usual. Trails routes the value across the edge rail and settles to the edge-chain recipient address — no edge wallet or adapter is involved on the user's side. ## Execution flow An edge-origin transaction follows the same intent lifecycle as an EVM flow (see [How Trails Works](/architecture/how-it-works)), with the origin leg executed on the edge chain: 1. **Quote.** The route is quoted across the edge rail. Edge origin and destination are both quoted through the same intent quote path; edge chains are addressed by their ecosystem-native chain ID. 2. **Origin execution.** The user signs and submits the origin transaction with their edge-chain wallet via the edge adapter. Funds enter the edge rail. 3. **Bridging & settlement.** Value crosses into the EVM intent graph (or vice-versa for edge destinations) and settles to the recipient, exactly as in a standard Trails flow. An edge-origin intent still designates an EVM address as the intent's refund owner, even though the user funds from a non-EVM wallet. This is an internal detail of how the intent is anchored; the funding and signing happen on the edge chain. ## Addresses and tokens Edge chains use their ecosystem-native conventions rather than EVM ones: * **Addresses** follow the edge chain's own format (for example, base58 rather than `0x`-prefixed hex). * **Native and token identifiers** use the edge chain's native scheme. Trails resolves known edge tokens from a built-in catalog rather than the EVM supported-tokens API. The SDK's `Token` and `Chain` types are chain-agnostic, so the same shapes carry both EVM and edge data. See [Tokens & Chains](/sdk/tokens-and-chains) for the SDK-side utilities. ## Adding new alternative VMs The edge-rail model is designed to grow without changing integration code: * New edge chains appear in the query results and quote paths as their rails go live. * Each ecosystem ships its own wallet adapter for edge-origin signing — see [Adapters](/sdk/adapters/overview) (e.g. the [Solana adapter](/sdk/adapters/solana)). * The per-hook support matrix in [Hooks → Edge chains](/sdk/hooks#edge-chains) is organized by capability, not by chain, so it applies to any edge chain. Need an alternative VM that isn't listed yet? [Contact us](https://t.me/build_with_trails) to request it. # How Trails Works Source: https://docs.trails.build/architecture/how-it-works Core architecture and design of the Trails transaction rails protocol Trails offers developers a solution to enable 1-click crypto experiences via any wallet, token and chain. Trails aims to make EVM chains "work as one" by solving the widespread chain, token, and wallet fragmentation problems by providing a multichain transaction orchestration layer on top of wallets powered by intents that unifies all chains and automates token routing, all with a single end-user confirmation (1-click). ## Overview Trails automatically determines the optimal path for cross-chain transaction execution, whether it's a simple single-chain transaction or a complex multi-step orchestration involving swaps, bridges, and executions across different blockchains. Trails is built on the [Sequence](https://docs.sequence.xyz) wallet stack combining the powers of account abstraction, real-time indexing, and gasless transaction execution into an intent-powered chain abstraction solution. Trails is designed to work with any wallet, including a simple EOA wallet, a powerful smart wallet or an in-app embedded wallet (ie. MetaMask, Wallet Connect, Privy, Coinbase Smart Wallet, etc). For more details under-the-hood, please see the [protocol architecture](/architecture/protocol) section. The protocol is designed to be extensible, with plans to support non-EVM chains in the future. ## Transaction flow A Trails transaction moves through two phases: origin chain and destination chain. The user is only involved in the first. **Intent address generation.** When a transaction is initiated, a unique intent contract address is computed from the full transaction orchestration across chains. This address encodes the details of both origin and destination transactions, making execution onchain-verifiable. The user's wallet is the only party with control over this address. **Origin transfer.** The user deposits tokens into the intent address on the origin chain. This is the only transaction the user signs. Everything from here — swaps, bridging, destination execution — is handled automatically. **Origin chain execution.** The Relayer executes the encoded origin transaction by submitting a Merkle proof to the intent address. The Relayer cannot modify transaction contents; any party can safely trigger execution. Swaps or bridge operations required on the origin chain are handled at this stage. **Bridging.** Funds move to the corresponding intent address on the destination chain via whatever bridge or liquidity route was selected at quote time. **Final settlement.** The Relayer executes the destination transaction the same way — Merkle proof, no ability to deviate. Funds are sent to the recipient, contract calls are executed, and any specified actions (mint, deposit, etc.) complete atomically. The reason users sign only once is that the intent address locks in the entire transaction graph upfront. The relayer executes what was cryptographically committed; it has no discretion to change it. ## Key Benefits Users interact with a simple interface while Trails handles complex cross-chain orchestration Intelligent algorithms find the most efficient paths across chains and protocols Minimizes fees and gas costs through smart routing and batching Non-custodial architecture ensures users maintain control of their assets ## Technical Architecture The Trails protocol leverages several key components: * **Intent Recognition**: Parses and understands intents * **Route Optimization**: Calculates optimal paths across supported chains * **Protocol Integration**: Interfaces with DEXs, bridges, and other protocols * **Transaction Orchestration**: Coordinates complex multi-step operations * **Execution Engine**: Handles the actual transaction execution and monitoring * **Intent Addresses**: Built using account abstraction technology, creates a self-custodial & computationally verifiable address that can orchestrate any arbitrary action. ## Integration with External Protocols & Liquidity Networks Trails is a composable solution which is able to plug-in to virtually any existing cross or same-chain infrastructure to efficiently route transactions. * **Trails as an Orchestrator**: Trails does not custody funds or act as a bridge/DEX itself. It orchestrates flows across existing liquidity networks and chains, acting as the glue that connects them so they work seamlessly as one. * **Liquidity Providers (LPs) & DEXs**: Trails taps into AMMs, bridges, aggregators, and LPs to source liquidity and pricing, selecting the optimal venue per step of the route, providing the best price and gas efficiency possible for the end-user. * **Pluggable Architecture**: New protocols, bridges, and networks can be added without changing code. Trails evaluates options at runtime and selects the safest, most efficient path, giving the developer full control over the flow. Trails is responsible for orchestrating the flow, not holding user assets. It coordinates protocols so users can transact with a single click (one transaction on the original chain), while assets move through underlying networks. ## Refunds Trails has an automatic refund mechanism built in to protect your assets in the event of a transaction failure. | Failure Scenario | Refund Action | | - | - | | **Failure on the Source Chain** | If the transaction fails before your funds leave the initial blockchain, the user will receive a full refund (minus gas fees) on the source chain to the sender address. | | **Failure on the Destination Chain** | If the funds successfully bridged but the final step fails on the destination chain, the assets are refunded to your sender address on the destination chain. | ## Security & Validation * **Built on Sequence v3**: Trails is built on top of Sequence's audited v3 wallet contracts, leveraging mature account abstraction and battle-tested controls. * **Intent-Scoped Authorization**: Intent addresses are only used to trustlessly authorize the specific transaction calldata the user has explicitly permitted—nothing more. * **User “sudo” Control**: The user always retains control over their account and funds. In emergencies, they can take direct action at any time. * **Non-Custodial by Design**: Trails never takes custody; execution happens via the user's wallet permissions, preserving full control and auditability. # Pricing & Fees Source: https://docs.trails.build/architecture/pricing-fees Understanding the fee structure and pricing model for Trails transactions ## Overview Trails is **free to integrate** for any application. There are no setup fees, monthly costs, or integration charges. All fees are paid by the end-user at transaction time and are transparently displayed before confirmation. ## Fee Breakdown Every Trails transaction includes the following fee components: | Fee Type | Description | Paid To | | - | - | - | | **Origin Gas** | Gas to execute the transaction on the source chain | Network Validators / Sequencer | | **Destination Gas** | Gas to execute the transaction on the target chain | Network Validators / Sequencer | | **Bridge/LP Fees** | Fees charged by the underlying liquidity provider or bridge | Bridge/Liquidity Providers | | **Trails Protocol Fee** | Small protocol fee that decreases with volume | Trails protocol | All fees are calculated and displayed upfront in the quote. Users see the exact amount they'll pay before confirming any transaction. ## Protocol Fee Structure The Trails protocol fee uses a volume-based tier system: * Fees scale **significantly down** as transaction volume increases * High-volume applications benefit from reduced per-transaction costs * Fee tiers are calculated based on USD volume transacted For enterprise pricing or high-volume discounts, contact the team. ## Fee Transparency Trails provides complete fee transparency through: * **Quote Display**: Full fee breakdown shown before transaction confirmation * **API Response**: Detailed fee information returned in quote responses (`totalFeeAmountUsdDisplay`) * **No Hidden Costs**: What you see in the quote is exactly what users pay ## Alternative Gas Tokens Users can pay gas fees using permit-compatible tokens like USDC or USDT instead of native tokens. See [Pay Gas with Any Token](/architecture/alt-fee-tokens) for details. # Protocol Architecture Source: https://docs.trails.build/architecture/protocol Core architecture and design of the Trails protocol ## Trails end-to-end Flow Trails enables seamless cross-chain transactions through a coordinated system of intent contracts, relayers, and solvers. Below is the complete flow from user initiation to transaction completion. (click here to view diagram in full screen) ![Trails end-to-end flow](https://pub-dc89be64e317442b832bec22d3d12052.r2.dev/trails-flow.svg) Additionally, you can think of Trails as a pluggable transaction adapter that can execute any arbitrary transactions across multiple chains. See below. ![Trails adapter](https://pub-dc89be64e317442b832bec22d3d12052.r2.dev/trails-adapter.svg) ## Under the Hood Under the hood, Trails intents are counterfactual instantiations of [Sequence v3 account abstraction](https://github.com/0xsequence/wallet-contracts-v3) contracts. The Sequence v3 account abstraction contracts introduce a very flexible and novel execution model powered by merkle trees. The contract execution configuration is represented as a merkle tree that includes signers, a digest, a module, or a combination of all. The execution of the intent is triggered by a single transaction to the intent address, where the Trails relayers observe the intent address and post merkle proofs onchain of the encoded sub-transactions on origin and destination chains. Please review the [Protocol Flow Overview](/architecture/protocol#trails-end-to-end-flow) section above for more details. In short, Trails introduces a novel 'intent address' primitive powered by account abstraction tech that enables fully onchain control and verifiable onchain execution transaction capabilities. Ultimately, this enables account abstraction primitives to EOA wallets (even EOA wallets without ERC7702) trustlessly. This is the magic of the Trails design in how it can construct a single transaction with a single end-user confirmation that spans multiple chains and transactions. The architectural goal of Trails is to always be able to construct a direct cross-chain/cross-token route for even the most common denominator of wallets, such as an EOA without ERC7702. Trails is architected as a trustless system based on intents that works on top of existing bridging and liquidity infrastructure by orchestrating these components together. ## Key Components ### Architecture Layers 1. **User Interaction Layer**: Handles user requests and wallet interactions 2. **SDK Layer**: Manages balance queries, token selection, and orchestration 3. **Solver Layer**: Provides optimal routing solutions for cross-chain transfers 4. **Chain Layer**: Coordinates relayer operations across multiple blockchains 5. **Execution Layer**: Executes intent contracts and completes transactions ### Protocol Flow The protocol follows a systematic approach: 1. **Initiation**: User initiates a cross-chain transaction through their app 2. **Discovery**: SDK queries available token balances across all supported chains 3. **Selection**: User selects preferred payment token from any available chain 4. **Solution**: Sequence API provides optimal bridge/swapsolution 5. **Execution**: Coordinated execution across origin and destination chains 6. **Completion**: Intent contracts fulfill the transaction requirements ### Intent-Based Architecture The protocol leverages intent contracts that: * Define transaction requirements without specifying execution paths * Enable flexible solver competition for optimal routes * Provide atomicity guarantees across chain boundaries * Support complex multi-hop operations seamlessly ### Cross-Chain Coordination Relayers on each chain work in concert to: * Monitor intent contract states * Execute transactions with cryptographic proofs * Ensure atomic completion or rollback * Maintain consistency across chain boundaries ### Role of the Relayer (How is this trustless?) * Relayers cannot alter the transaction calldata or intent contents in any way; they can only submit the pre-committed, verifiable calls that match the intent's merkle commitments. * Their responsibility is limited to subsidizing gas and successfully landing the transaction on the underlying chain on the user's behalf. * Any deviation from the committed parameters is rejected on-chain by the intent contracts, ensuring the relayer cannot manipulate execution. # Trails Contracts Source: https://docs.trails.build/architecture/smart-contracts Trails protocol smart contracts and security audits ## Overview Trails is built on a robust set of smart contracts deployed across multiple chains. All contracts are deployed via ERC-2470 Singleton Factory for deterministic addresses, ensuring consistency across the ecosystem. ## Contracts Repository The complete source code for all Trails smart contracts is available on GitHub:
View the full smart contract source code, documentation, and deployment scripts for Trails here View the full smart contract source code, documentation, and deployment scripts that powers account abstraction for Trails
### Key Contracts The Trails protocol consists of several core smart contracts: * **TrailsRouter** - Main routing contract for intent execution and token operations * **TrailsIntentEntrypoint** - Entry point for intent deposits with permit support * **TrailsRouterShim** - Execution wrapper with sentinel tracking for success/failure states * **HydrateProxy** - Runtime value injection for v1.5 intents. Resolves dynamic amounts and addresses (ERC-20 balances, intent wallet address) at execution time, enabling composable actions with `dynamic()` without committing exact amounts at quote time All contracts leverage Sequence v3 account abstraction for flexible, secure cross-chain operations. See [Protocol v1.5](/architecture/v1-5) for more detail on how HydrateProxy enables malleable calldata. ## Security Audits Security is a top priority for Trails. All smart contracts have undergone comprehensive third-party security audits and public code4rena bounty competitions:
Review detailed security audit reports from independent security firms for Trails contracts Review detailed security audit reports from independent security firms for Sequence wallet contracts
For a technical overview, see the [Protocol Architecture](/architecture/protocol) documentation. # Liquidity Sources Source: https://docs.trails.build/architecture/unified-liquidity How Trails integrates with Liquidity providers including solvers, fillers, and bridges ## Overview Trails acts as an orchestration layer that connects to multiple liquidity sources to find optimal routes for cross-chain transactions. Rather than building its own liquidity pools, Trails aggregates existing infrastructure to provide the best pricing and execution for users. ## Integrated Liquidity Providers Trails currently integrates with the following liquidity sources: | Provider | Type | Description | | - | - | - | | **Circle CCTP** | Bridge | Native USDC bridging with fast finality and no slippage for stablecoin transfers | | **Relay** | Bridge/Solver | Fast cross-chain transfers with solver-based execution | ## How Liquidity Routing Works When a user initiates a cross-chain transaction, Trails: 1. **Queries all integrated providers** for available routes and pricing 2. **Evaluates routes** based on speed, cost, and reliability 3. **Selects the optimal path** considering gas costs, bridge fees, and slippage 4. **Executes atomically** through the intent-based architecture Trails automatically handles fallback routing. If a primary route fails, the system can reroute through alternative providers when possible. ## Adding New Liquidity Sources Trails is designed with a pluggable architecture that allows new bridges, DEXs, and solvers to be integrated without code changes. New protocols are evaluated based on: * Security track record and audit status * Liquidity depth and pricing competitiveness * Speed and reliability of execution * Chain coverage and token support Want to integrate your bridge or DEX with Trails? Contact the team via the [Trails Telegram group](https://t.me/build_with_trails). ## Benefits of Aggregated Liquidity * **Best Execution**: Users always get competitive pricing across multiple sources * **Resilience**: No single point of failure for cross-chain transfers * **Broad Coverage**: Access to tokens and chains across all integrated providers * **Future-Proof**: New providers can be added as the ecosystem evolves # HydrateProxy and Composable Actions Source: https://docs.trails.build/architecture/v1-5 How HydrateProxy enables dynamic on-chain value injection, and how Composable Actions expose it as a typed developer interface. HydrateProxy is a contract module that makes destination calldata malleable: exact token amounts are resolved on-chain at execution time rather than locked in at quote time. Composable Actions are the typed developer interface over HydrateProxy — instead of encoding calldata and hydration payloads manually, you express destination execution as a list of typed steps. *** ## HydrateProxy When a cross-chain intent includes a destination contract call, the calldata must describe what to do with the bridged funds. The challenge: the exact amount that arrives at the destination isn't known when you build the intent — it depends on bridge output, fees, and route selection. Without HydrateProxy, you'd have to commit a fixed amount at quote time. If the actual bridged output differed from that figure, the destination call would fail or leave dust. HydrateProxy replaces fixed amounts with hydration instructions. Instead of committing `deposit(1000000)` into the signed payload, you commit a rule: "at execution time, read the ERC-20 balance of the intent wallet and substitute it at offset N in the calldata." The actual value is resolved on-chain when the funds arrive — whatever came through is what gets used. This is what `dynamic()` expresses at the SDK level: ```ts theme={null} import { lend, dynamic } from '0xtrails' const actions = [ lend({ marketId: 'base-usdc-aave-v3-lending', amount: dynamic(), // resolved to actual on-chain balance at execution }), ] ``` ### Security model HydrateProxy enforces that only the pre-committed hydration rules can modify the calldata. The relayer cannot inject arbitrary values — the substitution logic is locked into the signed payload and verified on-chain. What you commit is what executes. *** ## Composable Actions Composable Actions are the developer interface over HydrateProxy. Instead of encoding calldata and hydration payloads by hand, you express destination execution as a typed list of steps. The SDK handles encoding. ### With `useQuote` ```tsx theme={null} import { useQuote, dynamic, swap, lend } from '0xtrails' const { send, isLoadingQuote } = useQuote({ from: { chain: 'arbitrum', token: 'USDC' }, to: { chain: 'base', token: 'USDC', amount: '100' }, actions: [ swap({ tokenIn: 'USDC', tokenOut: 'WETH', amountIn: dynamic(), }), lend({ marketId: 'base-weth-aave-v3-lending', amount: dynamic(), }), ], }) ``` Bridge to Base USDC, swap to WETH on arrival, then supply the WETH to Aave — all from a single user confirmation on the origin chain. ### With `useTrailsSendTransaction` ```tsx theme={null} import { useTrailsSendTransaction, dynamic, deposit } from '0xtrails' const { sendTransaction, isPending } = useTrailsSendTransaction({ actions: [ deposit({ marketId: 'base-usdc-morpho-0x...-4626-vault', amount: dynamic(), }), ], }) // Trails modal opens — user picks source chain and token. sendTransaction({ to: recipientAddress, tokenAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC on Base tokenAmount: '100000000', // 100 USDC (6 decimals) }) ``` Whatever USDC arrives on Base gets deposited into the Morpho vault at the actual arrived amount — no prediction required. See the full [Composable Actions documentation](/sdk/composable-actions/overview) for available action builders, `dynamic()` and `self()` value helpers, and market discovery. *** ## PERMIT flow For ERC-20 tokens that implement [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (USDC, DAI, and most modern ERC-20s), the SDK requests a single gasless `permit` signature instead of an on-chain approval transaction. The permit is bundled with the intent and submitted together — token approval and intent initiation happen atomically in one user confirmation. The SDK detects permit support automatically. For tokens that don't implement EIP-2612, it falls back to a standard on-chain approval. No configuration is required. *** ## `intentProtocol` option `intentProtocol` is an optional override on `TrailsProvider` and `useQuote`. The current protocol (HydrateProxy, Composable Actions, and PERMIT flow) is the default — no configuration is needed. ```tsx theme={null} import { TrailsProvider } from '0xtrails' {children} ``` You can pass `intentProtocolVersion` per-quote in `useQuote` if you need to force a specific version for a particular call, but this is an edge case. # Gasless Transactions Source: https://docs.trails.build/examples/gasless Enable users to pay gas fees with ERC-20 tokens like USDC or USDT instead of native gas. ## Overview Gasless transactions let users pay network fees using ERC-20 tokens (USDC, USDT) instead of requiring native gas (ETH, MATIC). This eliminates a major friction point—users don't need to hold the native token on every chain. | Benefit | How It Works | | - | - | | No native gas required | Users pay fees in stablecoins or other ERC-20 tokens | | Better UX | No "insufficient gas" errors for users with only stablecoins | | Permit signatures | EIP-2612 permits enable gasless token approvals | | Flexible fee options | SDK shows available fee token options automatically | ## Basic Gasless Payment When using the SDK, gasless fee options are automatically displayed if the user's origin token supports permits: ```tsx theme={null} import { Pay } from '0xtrails/widget' export function GaslessPayment() { return ( { console.log('Payment complete:', sessionId) }} > ) } ``` When the user has USDC (a permit-compatible token), the SDK: 1. Shows fee options including "Pay gas in USDC" 2. Requests a permit signature (no approval transaction needed) 3. Submits via relayer—user's USDC covers the gas fee ## USDC/USDT as Gas Fee Examples ### Pay with USDC on Polygon ```tsx theme={null} { verifyPayment(sessionId) }} > ``` The user only needs USDC—no ETH required for gas. ### Pay with USDT on Polygon ```tsx theme={null} { verifyPayment(sessionId) }} > ``` ### Cross-Chain Gasless Even cross-chain payments can be gasless if the origin token supports permits: ```tsx theme={null} { verifyPayment(sessionId) }} > ``` User pays with USDC on Base → Trails bridges gaslessly → Merchant receives USDC on Polygon. ## Permit-Compatible Tokens Gasless transactions require tokens that implement EIP-2612 (permit): | Token | Permit Support | Notes | | - | - | - | | USDC | ✓ | Native Circle USDC on all supported chains | | USDT | ✓ | Most chains (verify per-chain) | | DAI | ✓ | Supports permit on Ethereum, L2s | | WETH | ✓ | Wrapped ETH supports permit | | Most ERC-20s | Varies | Check if token has `permit()` function | Native tokens (ETH, MATIC, etc.) cannot be used for gasless since they don't support permits. Users paying with native tokens will use standard gas. ## How Fee Options Work The SDK automatically fetches available fee options based on: * User's token balances on the origin chain * Which tokens support permit signatures * Current gas prices and token exchange rates Users see a "Gas Fee Options" dropdown with choices like: * ETH (native gas) * USDC (\$0.12) * USDT (\$0.12) Selecting an ERC-20 option triggers the gasless flow. ### Programmatic Fee Selection You can listen to fee option events: ```tsx theme={null} { console.log('Payment started:', sessionId) }} onSignRequest={() => { console.log('Permit signature requested') }} onSign={() => { console.log('Permit signed—submitting to relayer') }} onPaymentSuccess={({ sessionId }) => { console.log('Gasless payment complete:', sessionId) }} > ``` ## Gasless Flow Details 1. **Quote Intent** — SDK fetches quote including `gasFeeOptions` with available fee tokens 2. **User Selects Fee** — User picks USDC, USDT, or native gas from dropdown 3. **Permit Signature** — If ERC-20 fee selected, user signs EIP-2612 permit (no on-chain tx) 4. **Intent Signature** — User signs the intent data 5. **Relayer Submits** — Trails relayer executes the transaction, deducting fee from user's balance 6. **Completion** — User receives confirmation once destination transaction confirms ## Limitations **Smart Contract Wallets**: Gasless deposits via permit signatures require EOA (externally owned account) wallets. Smart contract wallets produce non-standard signatures that the current relayer doesn't support. ```tsx theme={null} // Gasless automatically falls back to native gas for smart contract wallets ``` The SDK automatically detects smart contract wallets and falls back to native gas flow. **Native Token Payments**: Sending ETH, MATIC, or other native tokens cannot be gasless—you need native gas to send native gas. ## Best Practices Don't force gasless. Some users prefer paying native gas (often cheaper at low congestion). The SDK shows both options by default. Users might reject the permit signature. Handle this gracefully: ```tsx theme={null} onSignReject={(error) => { console.log('User rejected signature:', error) // Show retry option or native gas alternative }} ``` Not all ERC-20 tokens support permits. The SDK automatically filters to show only valid fee options, but for API integrations, check token contracts. ## Troubleshooting | Issue | Cause | Solution | | - | - | - | | No ERC-20 fee options shown | Token doesn't support permit | User must pay native gas | | "Smart contract wallets not supported" | SC wallet detected | Use native gas flow | | Permit signature fails | Token permit implementation varies | Try a different token or native gas | ## Next Steps All Pay component options and examples Full configuration reference Chains and token support # Refunds & Recovery Source: https://docs.trails.build/examples/refunds How Trails handles automatic refunds and how users retain full control over their funds Trails is designed with user sovereignty at its core. While the protocol includes automatic refund mechanisms for failed transactions, users always retain full control over their funds and can initiate manual recovery at any time. ## Automatic Refunds Trails has an automatic refund mechanism built in to protect your assets in the event of a transaction failure. The protocol handles refunds automatically without requiring user intervention: | Failure Scenario | Refund Action | | - | - | | **Failure on the Source Chain** | If the transaction fails before your funds leave the initial blockchain, the user receives a full refund (minus gas fees) on the source chain to the sender address. | | **Failure on the Destination Chain** | If the funds successfully bridged but the final step fails on the destination chain, the assets are refunded to your sender address on the destination chain. | For more details on how refunds work within the Trails architecture, see the [How Trails Works](/architecture/how-it-works#refunds) documentation. ## User Control & Ownership Trails is **non-custodial by design**. Only the user always retains control over their account and funds: * **Intent-Scoped Authorization**: Intent addresses only authorize the specific transaction calldata the user has explicitly permitted—nothing more * **User Control**: Users can take direct action over their funds at any time, even in emergencies * **Non-Custodial**: Trails never takes custody; execution happens via the user's wallet permissions, preserving full control and auditability The user's wallet is the only party with control over the intent address. This cryptographic guarantee ensures the funds are always recoverable. ### Initiating a Refund There are two ways to initiate a refund for a pending or stuck intent: * **Via the Explorer** — Go to the [Trails Explorer](https://app.trails.build/explorer), connect the wallet you used to initiate the transaction, and navigate to the pending intent. From there, you can initiate a manual refund directly. * **Via the Widget** — If you're using an application with the Trails Widget embedded, head to the **History** section. The widget will automatically detect intents that are eligible for a refund and allow you to initiate one in-place. ## Manual Fund Recovery with useIntentRecover While Trails automatically refunds upon transaction failures, there may be edge cases where funds are stuck without an explicit revert. The `useIntentRecover` hook provides utilities to manually sign and execute recovery transactions to recover funds from intent addresses if you'd like to add to your own app. ### Basic Usage This example uses wagmi hooks (`useWalletClient`, `useAccount`). Since [`0xtrails@0.16.0`](/sdk/changelog/0xtrails-0.16.0), wagmi is opt-in — install `@0xtrails/adapter-wagmi` and pass `wagmiAdapter({ wagmiConfig })` in `adapters` for these hooks to resolve. See [SDK Hooks](/sdk/hooks#apps-with-wagmi). ```tsx theme={null} import { useIntentRecover } from '0xtrails' import { useWalletClient, useAccount } from 'wagmi' export const RefundComponent = ({ intentId }: { intentId: string }) => { const { data: walletClient } = useWalletClient() const { address } = useAccount() const { intent, isLoadingIntent, hasIntentBalance, signPayload, getRecoverTx, intentError, } = useIntentRecover({ intentId, walletClient, refundToAddress: address, // Optional: defaults to connected wallet }) const handleRefund = async () => { try { // Step 1: Sign the refund payload const { signature, payload } = await signPayload() // Step 2: Get the recovery transaction const recoverTx = await getRecoverTx({ signedHash: signature, payload, }) // Step 3: Execute the recovery transaction const txHash = await walletClient.sendTransaction({ to: recoverTx.to, data: recoverTx.data, chain: recoverTx.chain, }) console.log('Refund transaction sent:', txHash) } catch (error) { console.error('Refund failed:', error) } } if (isLoadingIntent) return
Loading intent...
if (intentError) return
Error: {intentError.message}
if (!hasIntentBalance) return
No balance to refund
return ( ) } ``` ## When to Use Manual Refunds **Most common scenario** — Trails automatically handles refunds when transactions explicitly fail or revert. No user action required. **Edge cases only** — Use `useIntentRecover` when funds are stuck without an explicit revert, or when you want to proactively recover funds from a pending intent. ## Security Guarantees The refund mechanism maintains Trails' security principles: * **Only the original sender** can authorize refunds via wallet signature * **Cryptographic verification** ensures refund transactions are valid * **Non-custodial** — Trails cannot access or move your funds without your signature * **Onchain execution** — All refund operations are verifiable onchain For more information, see: * [**Hooks Reference**](/sdk/hooks#useintentrecover) — Full documentation for `useIntentRecover` * [**How Trails Works**](/architecture/how-it-works) — Architecture overview including refund mechanisms * [**Protocol Architecture**](/architecture/protocol) — Deep dive into security and trust model # Tokens, Chains & Prices Source: https://docs.trails.build/examples/tokens-chains-prices Use Trails built-in indexer and methods to retrieve supported tokens, chains, and live prices Retrieve supported chains, tokens, and live USD prices using the SDK. ## Chains ```tsx theme={null} import { useSupportedChains, getSupportedChains, getChainInfo } from '0xtrails' // Hook: Get all supported chains const { supportedChains, isLoadingChains } = useSupportedChains() // Async function (non-React) const chains = await getSupportedChains() // Utility: Get specific chain info const base = getChainInfo(8453) console.log(base?.name) // "Base" ``` ## Tokens ```tsx theme={null} import { useTokenList, useSupportedTokens, getSupportedTokens } from '0xtrails' // All tokens across chains const { tokens, isLoadingTokens } = useTokenList() // Tokens for a specific chain const { supportedTokens } = useSupportedTokens({ chainId: 8453 }) // Async function (non-React) const allTokens = await getSupportedTokens() ``` ## Prices Token prices are included when fetching balances: ```tsx theme={null} import { useTokenBalances } from '0xtrails' const { sortedTokens } = useTokenBalances(address) sortedTokens.forEach(token => { console.log(`${token.symbol}: ${token.priceUsdDisplay}`) }) ``` For fetching prices without balances, use the [GetTokenPrices API](/api-reference/endpoints/get-token-prices) directly. ## Combined Example Build a chain/token selector with live prices: The `useAccount` import below is from wagmi. Since [`0xtrails@0.16.0`](/sdk/changelog/0xtrails-0.16.0), wagmi is opt-in — install `@0xtrails/adapter-wagmi` and configure `wagmiAdapter` to use wagmi hooks alongside Trails. See [SDK Hooks](/sdk/hooks#apps-with-wagmi). ```tsx theme={null} import { useState } from 'react' import { useSupportedChains, useSupportedTokens, useTokenBalances } from '0xtrails' import { useAccount } from 'wagmi' export const AssetSelector = () => { const { address } = useAccount() const [chainId, setChainId] = useState() const { supportedChains } = useSupportedChains() const { supportedTokens } = useSupportedTokens({ chainId }) const { sortedTokens } = useTokenBalances(address) return (
{/* Chain selector */} {/* Token list with balances */} {chainId && (
    {supportedTokens.map(token => { const withBalance = sortedTokens.find( t => t.contractAddress === token.contractAddress && t.chainId === chainId ) return (
  • {token.symbol} — {withBalance?.balanceDisplay ?? '0'} ({withBalance?.priceUsdDisplay ?? '$0'})
  • ) })}
)}
) } ``` For more details, see: * [**Tokens & Chains SDK Guide**](/sdk/tokens-and-chains) — Full SDK reference * [**Hooks Reference**](/sdk/hooks) — All available hooks * [**GetTokenPrices API**](/api-reference/endpoints/get-token-prices) — Fetch prices without balances # Intent & Transaction History Source: https://docs.trails.build/examples/transaction-history Retrieve account & intent transaction history for an address with the Trails SDK. Trails comes with a built in high-performant indexer which you can leverage seamlessly to retrieve information about a user's transaction history. The examples below import `useAccount` from wagmi. Since [`0xtrails@0.16.0`](/sdk/changelog/0xtrails-0.16.0), wagmi is opt-in — install `@0xtrails/adapter-wagmi` and configure `wagmiAdapter` to use wagmi hooks alongside Trails. See [SDK Hooks](/sdk/hooks#apps-with-wagmi). ## Wallet Transaction History Query a user's transaction history for a specific chain using their connected wallet address: ```tsx theme={null} import { useAccountTransactionHistory } from '0xtrails' import { useAccount } from 'wagmi' export const TransactionList = () => { const { address } = useAccount() const { data, isLoading } = useAccountTransactionHistory({ chainId: 137, accountAddress: address, }) if (isLoading) return
Loading...
return (
    {data?.transactions?.map((tx) => (
  • {tx.txnHash} | chain {tx.chainId} | block #{tx.blockNumber}
  • ))}
) } ``` ## Intent Transaction History Retrieve the full transaction history for a specific intent: ```tsx theme={null} import { useIntentTransactionHistory } from '0xtrails' import { useAccount } from 'wagmi' export const IntentHistory = ({ intentId }: { intentId: string }) => { const { address } = useAccount() const { transactions, loading, error, refetch, } = useIntentTransactionHistory({ accountAddress: address, pageSize: 10, enabled: true, }) if (loading) return
Loading intent history...
if (error) return
Error: {String(error)}
return (
    {transactions?.map((tx) => (
  • {tx.txnHash} | {tx.timestamp}
  • ))}
) } ``` For more information, see: * [**Hooks Reference**](/sdk/hooks) — Full documentation for transaction history hooks * [**Get Intent History API**](/api-reference/endpoints/get-intent-history) — API endpoint reference # Wallet Compatibility Source: https://docs.trails.build/examples/wallet-compatibility Comprehensive wallet integration support and compatibility across different blockchain ecosystems Trails works with virtually all wallets, including embedded wallets like Privy, smart contract wallets like Sequence, or injected EOAs like MetaMask. This enables complex transaction orchestration cross-chain in 1-click without requiring wallets to natively support ERC-7702. Since `0xtrails@0.16.0`, Trails uses its own built-in EVM runtime by default — wagmi is no longer required. The patterns below still work for apps that share an existing wagmi session, via the `@0xtrails/adapter-wagmi` package. See the [0.16 release notes](/sdk/changelog/0xtrails-0.16.0) for the migration. ## How It Works Trails detects and uses connected wallets through its built-in runtime, or through wagmi connectors when you install the wagmi adapter. For apps that already use wagmi, wrap your app with a wagmi provider, include the desired wallet connectors in your config, and pass `wagmiAdapter({ wagmiConfig })` to `TrailsProvider`. ## Basic Wagmi Setup ```tsx theme={null} import { createConfig, http, WagmiProvider } from 'wagmi' import { mainnet, base, arbitrum } from 'wagmi/chains' import { injected, walletConnect } from 'wagmi/connectors' import { Pay } from '0xtrails/widget' const config = createConfig({ chains: [mainnet, base, arbitrum], connectors: [ injected(), walletConnect({ projectId: 'your-walletconnect-project-id' }) ], transports: { [mainnet.id]: http(), [base.id]: http(), [arbitrum.id]: http(), }, }) export function App() { return ( ) } ``` ## Sequence Wallet Integration Integrate Sequence smart contract wallets for enhanced security and user experience: ```tsx theme={null} import { createConfig, http, WagmiProvider } from 'wagmi' import { mainnet, base, arbitrum } from 'wagmi/chains' import { sequenceWallet } from '@0xsequence/wagmi-connector' import { Pay } from '0xtrails/widget' // Create Sequence connector const sequenceConnector = sequenceWallet({ connectOptions: { app: "Your App Name", apiKey: "your-sequence-project-key", }, defaultNetwork: 1, // Mainnet walletAppURL: "https://sequence.app", }) const config = createConfig({ chains: [mainnet, base, arbitrum], connectors: [sequenceConnector], transports: { [mainnet.id]: http(), [base.id]: http(), [arbitrum.id]: http(), }, }) export function AppWithSequence() { return ( ) } ``` ## Privy Embedded Wallets Integrate Privy for seamless embedded wallet experiences. You can try live demos below: * [Headless SDK demo](https://demo-trails-privy-embedded.0xsequence.workers.dev/) * [Widget demo](https://demo-trails-modal-privy-embedded.0xsequence.workers.dev/) ```tsx theme={null} import React from 'react'; import { PrivyProvider } from '@privy-io/react-auth'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { mainnet } from 'wagmi/chains'; import { createConfig, WagmiProvider } from '@privy-io/wagmi'; import { http } from 'wagmi'; import { Pay } from '0xtrails/widget'; const config = createConfig({ chains: [mainnet], transports: { [mainnet.id]: http(), }, }); const queryClient = new QueryClient(); export function AppWithPrivy() { return ( ) } ``` ## Multiple Wallet Support Combine multiple wallet connectors to give users choice: ```tsx theme={null} import { createConfig, http, WagmiProvider } from 'wagmi' import { mainnet, base, arbitrum } from 'wagmi/chains' import { injected, walletConnect } from 'wagmi/connectors' import { sequenceWallet } from '@0xsequence/wagmi-connector' import { createPrivyWalletConnector } from '@privy-io/wagmi-connector' import { PrivyProvider } from '@privy-io/react-auth' import { Pay } from '0xtrails/widget' // Create all connectors const sequenceConnector = sequenceWallet({ connectOptions: { app: "Your App", apiKey: "your-sequence-key", }, defaultNetwork: 1, }) const privyConnector = createPrivyWalletConnector({ appId: "your-privy-app-id", name: "Privy", }) const config = createConfig({ chains: [mainnet, base, arbitrum], connectors: [ injected(), walletConnect({ projectId: 'your-walletconnect-project-id' }), sequenceConnector, privyConnector, ], transports: { [mainnet.id]: http(), [base.id]: http(), [arbitrum.id]: http(), }, }) export function MultiWalletApp() { return ( ) } ``` ## Supported Wallet Types Trails works with all wagmi-compatible wallets including: * **Injected Wallets**: MetaMask, Coinbase Wallet, Brave Wallet * **WalletConnect**: Any wallet supporting WalletConnect protocol * **Smart Contract Wallets**: Sequence, Safe, Biconomy * **Embedded Wallets**: Privy, Dynamic, Magic * **Mobile Wallets**: Rainbow, Trust Wallet, Phantom * **Hardware Wallets**: Ledger, Trezor (via connectors) ## Key Benefits * **Universal Compatibility**: Works with any wagmi connector * **No Wallet Modifications**: Wallets don't need ERC-7702, 4337, or other external dependencies * **Cross-Chain Transactions**: Complex multi-chain operations in 1-click * **Flexible Integration**: Use existing wallet infrastructure * **Future-Proof**: Automatically supports new wagmi connectors For more configuration options, see our [Configuration Guide](/sdk/configuration). # x402 Payments Source: https://docs.trails.build/examples/x402 Learn how to integrate Trails with gasless HTTP payment authentication using x402 # What is x402? x402 is an HTTP payment protocol that enables **gasless, signature-based payments** to authenticate API requests. Instead of on-chain transactions, users sign an EIP-712 message that authorizes a payment facilitator to transfer tokens on their behalf. Key benefits: * ✅ No gas fees for users * ✅ Instant payment verification * ✅ Works over standard HTTP * ✅ Perfect for API monetization ## Why Combine Them? Many facilitators only accept specific tokens on specific chains. In addition, x402 requires EIP-3009 on the token contract further limiting token support. However, Trails enables x402 using any token from any chain. Trails + x402 creates a powerful payment flow: 1. **User pays** with Trails using any token from any chain 2. **Payment arrives** on your chosen chain and token that the facilitator supports (e.g., Base USDC) 3. **Automatic trigger** of x402 signature for API access 4. **Gasless authentication** to your protected endpoints 5. **API responds** with paid content/service ## Architecture ``` ┌──────────────┐ │ Frontend │ │ (React) │ └──────┬───────┘ │ │ 1. User clicks "Pay & Get Access" │ ▼ ┌──────────────────────┐ │ Trails Widget │ │ (Cross-chain Pay) │ └──────┬───────────────┘ │ │ 2. Routes payment from any chain/token │ to Base USDC (or your target) │ ▼ ┌──────────────────────┐ │ onPaymentSuccess │ │ (Callback) │ └──────┬───────────────┘ │ │ 3. Triggers x402 signature │ ▼ ┌──────────────────────┐ │ x402-axios │ │ (Payment Signer) │ └──────┬───────────────┘ │ │ 4. Adds payment header to HTTP request │ ▼ ┌──────────────────────┐ │ Backend API │ │ (x402-hono) │ └──────┬───────────────┘ │ │ 5. Verifies payment & transfers tokens │ ▼ ┌──────────────────────┐ │ Protected Response │ │ (API Data) │ └──────────────────────┘ ``` ## Implementation ### Frontend Setup #### 1. Install Dependencies ```bash theme={null} pnpm add 0xtrails x402-axios axios ``` This example pairs Trails with an existing wagmi + RainbowKit setup. Since [`0xtrails@0.16.0`](/sdk/changelog/0xtrails-0.16.0), wagmi is opt-in for Trails — also install `@0xtrails/adapter-wagmi` so Trails can share the wagmi session: ```bash theme={null} pnpm add @0xtrails/adapter-wagmi ``` #### 2. Configure Providers ```tsx Providers.tsx theme={null} import { WagmiProvider, createConfig, http } from "wagmi" import { RainbowKitProvider } from "@rainbow-me/rainbowkit" import { QueryClientProvider, QueryClient } from "@tanstack/react-query" import { mainnet, base } from "wagmi/chains" import { TrailsProvider } from "0xtrails" import { wagmiAdapter } from "@0xtrails/adapter-wagmi" const config = createConfig({ chains: [mainnet, base], transports: { [mainnet.id]: http(), [base.id]: http(), [baseSepolia.id]: http(), }, }) const queryClient = new QueryClient() const adapters = [wagmiAdapter({ wagmiConfig: config })] export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` #### 3. Create x402 API Client ```ts services/api.ts theme={null} import axios from "axios" import type { WalletClient } from "viem" import { withPaymentInterceptor } from "x402-axios" const baseApiClient = axios.create({ baseURL: "http://localhost:3001", headers: { "Content-Type": "application/json" }, }) let apiClient = baseApiClient // Update API client when wallet connects export function updateApiClient(walletClient: WalletClient | null) { if (walletClient && walletClient.account) { // Attach x402 payment interceptor to axios apiClient = withPaymentInterceptor(baseApiClient, walletClient as any) console.log("💳 API client ready for payments") } else { apiClient = baseApiClient } } // API endpoints export const api = { // Protected endpoint - requires x402 payment purchaseOneTimeAccess: async () => { const response = await apiClient.post("/api/pay/onetime") return response.data }, } ``` The `withPaymentInterceptor` automatically handles EIP-712 signature creation and attaches payment headers to HTTP requests. #### 4. Build Payment Widget Component ```tsx components/PayWidget.tsx theme={null} import { Pay } from "0xtrails/widget" import type { TrailsWidgetRef } from "0xtrails/widget" import { useState, useCallback, useRef } from "react" import { useWalletClient, useSwitchChain } from "wagmi" import { base } from "wagmi/chains" import { api, updateApiClient } from "../services/api" export default function PayWidget() { const widgetRef = useRef(null) const [loading, setLoading] = useState(false) const [result, setResult] = useState("") const { data: walletClient } = useWalletClient() const { switchChainAsync } = useSwitchChain() // Handle Trails payment completion const handleTrailsComplete = useCallback( async (data: { sessionId: string }) => { console.log("✅ Trails payment complete:", data.sessionId) // Close Trails modal widgetRef.current?.closeModal?.() // Switch to payment network (Base) try { await switchChainAsync?.({ chainId: base.id }) } catch (err) { console.error("Network switch failed:", err) } if (!walletClient) { setResult("❌ Wallet not connected") return } try { setLoading(true) setResult("🔄 Authorizing API access via x402...") // Update API client with wallet for x402 signing updateApiClient(walletClient) // Make payment-protected API request // x402-axios will automatically: // 1. Prompt user to sign EIP-712 message // 2. Add payment signature to request headers // 3. Facilitator verifies and transfers tokens const response = await api.purchaseOneTimeAccess() setResult(`✅ API access granted! Session: ${response.sessionId}`) } catch (error: any) { console.error("x402 payment error:", error) setResult(`❌ Payment failed: ${error.message}`) } finally { setLoading(false) } }, [walletClient, switchChainAsync] ) return (
{loading &&

Processing...

} {result &&

{result}

}
) } ```
The `onPaymentSuccess` callback is the bridge between Trails and x402. It fires when the cross-chain payment succeeds, then triggers the x402 signature flow. ### Backend Setup #### 1. Install Dependencies ```bash theme={null} pnpm add hono @hono/node-server x402-hono dotenv uuid ``` #### 2. Configure Environment ```env server/.env theme={null} # Your wallet address to receive x402 payments ADDRESS=0xYourWalletAddress # Network for x402 payments NETWORK=base # x402 Facilitator URL FACILITATOR_URL=https://facilitator.payai.network # Server Port PORT=3001 ``` #### 3. Create x402 Protected API ```ts server/index.ts theme={null} import { Hono } from "hono" import { serve } from "@hono/node-server" import { cors } from "hono/cors" import { paymentMiddleware, Network, Resource } from "x402-hono" import { v4 as uuidv4 } from "uuid" import { config } from "dotenv" config() const facilitatorUrl = process.env.FACILITATOR_URL as Resource const payTo = process.env.ADDRESS as `0x${string}` const network = process.env.NETWORK as Network || "base" const port = parseInt(process.env.PORT || "3001") const app = new Hono() // Enable CORS for frontend app.use("/*", cors({ origin: ["http://localhost:5173"], credentials: true, })) // In-memory session storage (use Redis/DB in production) const sessions = new Map() // Configure x402 payment middleware app.use( paymentMiddleware( payTo, // Your wallet address { // Define paid endpoints and prices "/api/pay/onetime": { price: "$0.10", network, }, "/api/pay/session": { price: "$1.00", network, }, }, { url: facilitatorUrl, // x402 facilitator } ) ) // Protected endpoint - requires x402 payment ($0.10) app.post("/api/pay/onetime", async (c) => { // This code only runs if payment succeeds! const sessionId = uuidv4() const now = new Date() const session = { id: sessionId, createdAt: now, expiresAt: new Date(now.getTime() + 5 * 60 * 1000), // 5 min type: "onetime", } sessions.set(sessionId, session) // Return protected data/service return c.json({ success: true, sessionId, message: "Payment verified! Access granted.", data: { // Your protected API response here exampleData: "This is only visible after payment", }, }) }) // Free endpoint - health check app.get("/api/health", (c) => { return c.json({ status: "ok", network, payTo, }) }) serve({ fetch: app.fetch, port }) console.log(`🚀 x402 Server running on port ${port}`) ``` The `paymentMiddleware` intercepts requests to protected routes and verifies the x402 payment signature before allowing access. ## Payment Flow Breakdown ### Step 1: User Initiates Payment (Trails) ```tsx theme={null} ``` The user clicks the Trails button and: * Selects their source chain and token (e.g., ETH on Arbitrum) * Trails calculates optimal route (swap → bridge → execute) * User approves transaction * Payment arrives as USDC on Base Trails handles all complexity: token swaps, bridge routing, gas estimation, and delivery confirmation. ### Step 2: Trails Callback Triggers x402 ```tsx theme={null} const handleTrailsComplete = useCallback(async (data) => { // Update API client with wallet for signing updateApiClient(walletClient) // Make x402-protected API request const response = await api.purchaseOneTimeAccess() }, [walletClient]) ``` When `onPaymentSuccess` fires: 1. API client is configured with the user's wallet 2. HTTP request is made to protected endpoint 3. x402 interceptor prompts for EIP-712 signature 4. Signature is attached to request headers ### Step 3: x402 Signature Creation ```ts theme={null} // x402-axios handles this automatically withPaymentInterceptor(axiosInstance, walletClient) ``` The interceptor: 1. Detects `402 Payment Required` response 2. Parses payment details from response headers 3. Creates EIP-712 typed data structure 4. Prompts user to sign (gasless, off-chain) 5. Retries request with payment signature Users sign a message, not a transaction. This is **gasless** and instant. ### Step 4: Backend Verification ```ts theme={null} app.use( paymentMiddleware( payTo, { "/api/pay/onetime": { price: "$0.10", network: "base" }, }, { url: facilitatorUrl } ) ) ``` The middleware: 1. Validates payment signature 2. Sends transfer request to facilitator 3. Facilitator transfers tokens from user → your wallet 4. If successful, request proceeds to handler 5. If failed, returns `402 Payment Required` ### Step 5: Protected Response ```ts theme={null} app.post("/api/pay/onetime", (c) => { // Payment verified! Return protected data return c.json({ success: true, data: "Your protected content", }) }) ``` Only accessible if payment succeeds. ## Conclusion Combining Trails and x402 creates a seamless payment experience: * **Trails** handles the complexity of cross-chain payments * **x402** provides gasless, instant API authentication * **Together** they enable truly user-friendly paid services - paid with any token from any chain. This pattern works for: * 🔐 API monetization * 📰 Content paywalls * 🎮 Gaming microtransactions * 📊 Data services * 🤖 AI API access * 💼 SaaS applications Start building payment-gated services today! # Quickstart Source: https://docs.trails.build/get-started Integrate Trails in under 5 minutes ## Install ```shell npm theme={null} npm install 0xtrails ``` ```shell pnpm theme={null} pnpm add 0xtrails ``` ```shell yarn theme={null} yarn add 0xtrails ``` ```shell bun theme={null} bun add 0xtrails ``` ## Add a component Pick the component that matches your use case and import it from `0xtrails/widget`: ```tsx theme={null} import { Fund } from '0xtrails/widget' export function Deposit() { return ( ) } ``` User deposits any token from any chain. Arrives as USDC on Base. **Get an API key**: Sign up and create an API key on the [Trails Dashboard](https://dashboard.trails.build/landing) ## Components | Component | Use case | | - | - | | `` | Accept a fixed payment (e.g. 25 USDC) to a recipient | | `` | Deposit funds into a wallet, chain, or protocol | | `` | Cross-chain token exchange | | `` | Off-ramp or move funds out | | `` | DeFi yield deposits | All are imported from `0xtrails/widget`. ## Starter Templates Try it now Starter kit Starter kit ## Next Steps Stablecoin payments, merchant integration Cross-chain token swaps Protocol deposits, chain onboarding, built-in onramps Integrated DeFi yield deposits *** **Full reference**: [SDK Configuration](/sdk/configuration) · [API Reference](/api-reference/introduction) · [Hooks](/sdk/hooks) # What is Trails? Source: https://docs.trails.build/intro The onchain transaction rails for every token from every chain with every wallet with one, aggregated balance - powered by intents. ## Overview Trails is an intent-powered orchestration protocol that enables 1-click transactions from any wallet seamlessly with unified user liquidity across all chains. In contrast to typical cross-chain solutions, Trails sources onchain liquidity and aggregates all user balances for every token across every chain in a user's wallet as options for any transaction. Trails natively integrates and complements existing bridging, filler, or solver infrastructure by sourcing them as liquidity and contributing volume. Trails is free to integrate and is optimized for a variety of use cases for the most common flows, for example: * **[Pay](/use-cases/pay)**: Enable cross-chain, 1-click payments with any token. Ideal for ecommerce platforms, NFT marketplaces, real-world asset purchases, and minimal-slippage stablecoin transactions. * **[Swap](/use-cases/swap)**: Embed low-latency, highly liquid cross-chain token swaps for your applications. * **[Fund](/use-cases/fund)**: Maximize TVL and transaction velocity by funding via fiat or crypto such as perpetual exchanges, prediction markets, and chains. Utilizing our intent address primitive, we are able to support nearly any token or chain - even if the onramp provider doesn't. * **[Earn](/use-cases/earn)**: Streamline DeFi yield opportunities by enabling deposits into tokenized vaults, lending pools, and yield strategies from any token on any chain already integrated into Trails. ## Key Features ### 1-click Transactions * Transactions occur in a single confirmation with any wallet, regardless if it's an EOA, account abstraction wallet, or EIP-7702 enabled. * Developers simply specify payment intent (e.g., "purchase NFT #123 with 30 USDC") and a user can select any token across their wallet balance to pay. ### Universal User Liquidity * Built-in multichain, realtime indexing to automatically return user's wallet balances, prices, and asset metadata. No third-party dependencies necessary. * Leveraging existing liquidity infrastructure such as DEX's, bridges, or aggregators, we support a vast array of tokens and chains. ### Simplified Developer and User Experience * Leverage any permit-compatible token to use as a gas fee end to end such as USDC or USDT. * Optimized UX flows for core use cases in pay, swap, fund, and earn. ### Blazing Fast * Fast from anywhere, even with payments across chains using our intent-based architecture & relayer infrastructure. * Protocol intelligently detects optimal route for payment fulfillment and presents to the user in a streamlined UX. ### Scalable * Trails can leverage virtually any existing cross-chain or liquidity infrastructure, including bridges, aggregators, and more. * Relayer & Indexing infrastructure is lightweight and easily deployable to new chains. ## How Trails Works Trails introduces a novel 'intent address' primitive powered by account abstraction tech that enables onchain verifiable execution trustlessly in 1-click from the user. Here's a simplified view on how Trails works: 1. **App Defines Intent**: Create a payment transaction (mint, deposit, swap, etc) for any chain in any token. 2. **Trails Inspects**: Can this succeed with the assets held on current chain? Yes = Direct Call. No = Continue Orchestration. 3. **User Selects Route**: Trails presents an aggregated balance of the user's assets across all chains for the user to select from. 4. **Trails Orchestrates Transaction**: Intelligently combines and optimizes swap + bridge + execute for the selected route to fulfill the transaction. 5. **Execute Transaction**: User confirms the transaction via their wallet which deposits the amount requested into a Trails 'intent address'. Trails handles any swaps, bridges, or executions automatically! This architecture enables developers to build sophisticated transaction experiences while abstracting the underlying complexity of multichain operations. All you have to do is specify what transaction and any payment options to execute whether cross or same chain. Developers can pass in calldata to execute any arbitrary smart contract functions. For more details, please check out the dedicated page to [learn more](/architecture/how-it-works). ## Compatibility Trails is complementary to existing cross-chain and chain abstraction infrastructure. It is highly pluggable and leverages various fillers, solvers, DEXs, and bridges for maximum liquidity and low-slippage across a wide variety of tokens and routes. In short, Trails enhances the flow of funds for the underlying infrastructure. **Wallet Support**: Trails works with any wallet including EOA wallets (MetaMask), smart wallets (Coinbase Smart Wallet), and embedded wallets (Privy). EIP-7702 is **not required**. **Chain Support**: Trails supports any EVM-compatible chain. See [supported chains](/resources/supported-chains) for the full list. ## Quickstart Looking to integrate? [Get started](/get-started) and build your app natively cross-chain in minutes. ## Who Built Trails? Trails is built by [Sequence](https://sequence.xyz), the all-in-one developer platform for crypto. # Cross-Chain Execution Source: https://docs.trails.build/overview/cross-chain-execution Overview of cross-chain execution capabilities, transaction routing, and multi-blockchain coordination mechanisms ## Overview Trails doesn't just move tokens cross-chain—it can execute arbitrary smart contract functions on the destination chain as part of the same transaction flow. This enables powerful use cases like cross-chain NFT mints, DeFi deposits, and protocol interactions. ## How It Works When you include `to.calldata` in your Trails transaction, the protocol: 1. **Routes funds** from the user's selected source chain/token 2. **Bridges and swaps** as needed to the destination chain/token 3. **Executes your calldata** on the destination contract with the funds attached All of this happens atomically from the user's perspective—one confirmation, one transaction. ## Example: Cross-Chain NFT Mint A user on Arbitrum wants to mint an NFT on Base that costs 0.01 ETH. They only have USDC: ```tsx theme={null} import { Pay } from '0xtrails/widget' import { encodeFunctionData } from 'viem' const mintCalldata = encodeFunctionData({ abi: [{ name: 'mint', type: 'function', stateMutability: 'payable', inputs: [{ name: 'to', type: 'address' }], outputs: [], }], functionName: 'mint', args: ['0xUserAddress'], }) ``` **What happens:** 1. User confirms a single transaction on Arbitrum 2. Trails swaps USDC → ETH and bridges to Base 3. On Base, Trails calls `mint()` on the NFT contract with 0.01 ETH attached 4. User receives their NFT ## Example: Cross-Chain DeFi Deposit Deposit into an Aave lending pool on Arbitrum using tokens from any chain: ```tsx theme={null} import { Fund } from '0xtrails/widget' import { TRAILS_HYDRATE_PLACEHOLDER_AMOUNT } from '0xtrails' import { encodeFunctionData } from 'viem' 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: [ '0xUSDCAddress', TRAILS_HYDRATE_PLACEHOLDER_AMOUNT, // Replaced with actual amount at execution '0xUserAddress', 0, ], }) ``` ## Supported Calldata Patterns ### Static Calldata When the function parameters are known in advance: * NFT mints with fixed recipient * Staking with fixed parameters * Any function where amount isn't a parameter ### Dynamic Calldata When the amount needs to be determined at execution time, use `TRAILS_HYDRATE_PLACEHOLDER_AMOUNT`: * ERC-4626 vault deposits * Lending protocol supplies * Any function with an `amount` parameter that should match bridged funds ## Key Benefits * **Single Transaction**: Users confirm once, regardless of complexity * **Any Token → Any Function**: Pay with USDC, execute with ETH * **Atomic Execution**: Calldata executes only if funds arrive successfully * **No Contract Changes**: Works with existing smart contracts ## Use Cases | Use Case | Example | | - | - | | **NFT Minting** | Mint on any chain with any token | | **DeFi Deposits** | Supply to lending protocols cross-chain | | **Staking** | Stake tokens on any chain | | **Gaming** | Purchase in-game items across chains | | **DAO Interactions** | Vote or participate with any token | ## See Also * [Pay Mode](/sdk/modes/pay) - Configuration for payment with calldata * [Fund Mode](/sdk/modes/fund) - Configuration for deposits with calldata * [How It Works](/architecture/how-it-works) - Detailed protocol architecture # LLM Integrations Source: https://docs.trails.build/resources/llm-integration Integrate Trails with AI agents for automated payments, from MCP servers to custom function calling. ## llms.txt Trails provides an `llms.txt` file for AI-friendly documentation discovery. This file follows the [llms.txt standard](https://llmstxt.org/) and provides structured context about Trails capabilities. **URL:** [https://docs.trails.build/llms.txt](https://docs.trails.build/llms.txt) Use this file to give your LLM context about Trails before making integration requests. It includes: * Core concepts (intents, routes, SDK modes) * API flow overview * Common integration patterns * Links to relevant documentation ## MCP Server The Trails MCP Server allows you to leverage AI agents like Claude or Cursor seamlessly with Trails. The server is available at: `https://docs.trails.build/mcp` ### Connect in Cursor 1. Use Command + Shift + P (Ctrl + Shift + P on Windows) to open the command palette. 2. Search for "Open MCP settings". 3. Select **Add custom MCP** to open the `mcp.json` file. 4. In `mcp.json`, add the Trails server: ```json theme={null} { "mcpServers": { "Trails": { "url": "https://docs.trails.build/mcp" } } } ``` 5. In Cursor chat, ask "What tools do you have available?" to confirm the Trails MCP server is listed. See the [Cursor documentation](https://docs.cursor.com/en/context/mcp#installing-mcp-servers) for more details. ### Connect in Claude 1. Navigate to the \[Developers] page in Claude Desktop. 2. Select **Edit config** if you have an existing MCP configuration or add a new one. 3. Open your config file and add the Trails MCP server: * Name: `Trails` * URL: `https://docs.trails.build/mcp` 4. Save the configuration 5. In a Claude chat, select the `Search and tools` and make sure your configuration file with Trails is enabled. 6. Ask a question about Trails! See the [Model Context Protocol docs](https://modelcontextprotocol.io/docs/tutorials/use-remote-mcp-server#connecting-to-a-remote-mcp-server) for more details. ### Example Queries You can ask questions like: * "How would I add a DeFi vault for USDC on Aave to my react app using Trails?" * "What theming options are available for Trails?" * "What chains does trails support?" When your query is being processed, you'll see an indication like: ``` > Called SearchTrails ``` ## Function Calling Tools For custom LLM integrations (OpenAI, Anthropic, etc.), use these tool definitions to enable payment capabilities. ### Payment Tool Definition ```json theme={null} { "name": "create_payment", "description": "Create a crypto payment request. Accepts any token from any chain, settles in stablecoins. Use for: sending payments, paying invoices, e-commerce checkout.", "parameters": { "type": "object", "properties": { "recipient_address": { "type": "string", "description": "Ethereum address to receive payment (0x...)" }, "amount": { "type": "string", "description": "Amount to receive in destination token (e.g., '100' for 100 USDC)" }, "token": { "type": "string", "description": "Token symbol to receive (USDC, USDT, ETH, etc.)", "default": "USDC" }, "chain_id": { "type": "number", "description": "Destination chain ID (1=Ethereum, 8453=Base, 42161=Arbitrum)", "default": 8453 }, "memo": { "type": "string", "description": "Optional payment reference or description" } }, "required": ["recipient_address", "amount"] } } ``` ### Quote Tool Definition ```json theme={null} { "name": "get_payment_quote", "description": "Get a quote for a cross-chain payment. Returns estimated fees, exchange rates, and routing info before execution.", "parameters": { "type": "object", "properties": { "from_token": { "type": "string", "description": "Token symbol user wants to pay with" }, "from_chain_id": { "type": "number", "description": "Chain ID where user's tokens are" }, "to_token": { "type": "string", "description": "Token symbol recipient will receive" }, "to_chain_id": { "type": "number", "description": "Chain ID for recipient" }, "amount": { "type": "string", "description": "Amount (in destination token for EXACT_OUTPUT, source for EXACT_INPUT)" }, "trade_type": { "type": "string", "enum": ["EXACT_INPUT", "EXACT_OUTPUT"], "description": "EXACT_OUTPUT: specify what recipient gets. EXACT_INPUT: specify what user pays.", "default": "EXACT_OUTPUT" } }, "required": ["from_token", "from_chain_id", "to_token", "to_chain_id", "amount"] } } ``` ### Swap Tool Definition ```json theme={null} { "name": "swap_tokens", "description": "Swap tokens across chains. User specifies input token and amount, receives output token on any chain.", "parameters": { "type": "object", "properties": { "from_token": { "type": "string", "description": "Token symbol to swap from" }, "from_chain_id": { "type": "number", "description": "Source chain ID" }, "from_amount": { "type": "string", "description": "Amount of source token to swap" }, "to_token": { "type": "string", "description": "Token symbol to receive" }, "to_chain_id": { "type": "number", "description": "Destination chain ID" }, "slippage_tolerance": { "type": "number", "description": "Max slippage as decimal (0.005 = 0.5%)", "default": 0.005 } }, "required": ["from_token", "from_chain_id", "from_amount", "to_token", "to_chain_id"] } } ``` ## Example Prompts These prompts help LLMs understand common payment scenarios. ### System Prompt for Payment Agent ``` You are a payment assistant that helps users send crypto payments using Trails. Key capabilities: - Accept any token from any chain - Settle payments in stablecoins (USDC/USDT) - Cross-chain swaps and bridges handled automatically - Gasless transactions available for permit-compatible tokens When users want to make a payment: 1. Confirm the recipient address and amount 2. Ask what token/chain they want to pay WITH (or suggest based on their balances) 3. Get a quote to show fees and rates 4. Execute when user confirms Always show the fee breakdown before executing. Never execute without user confirmation. ``` ### User Intent Examples | User says | Interpreted action | | - | - | | "Pay 0xABC 50 USDC" | `create_payment(recipient="0xABC", amount="50", token="USDC")` | | "Send 100 dollars to merchant.eth" | Resolve ENS, then `create_payment(amount="100", token="USDC")` | | "Swap my ETH on mainnet to USDC on Base" | `swap_tokens(from="ETH", from_chain=1, to="USDC", to_chain=8453)` | | "How much will it cost to send 1000 USDC?" | `get_payment_quote(to_token="USDC", amount="1000")` | | "Pay invoice #123 for 250 USDT" | `create_payment(amount="250", token="USDT", memo="Invoice #123")` | ## API Integration Example Complete example implementing the payment tool with Trails API: ```typescript theme={null} import { TrailsApi, TradeType } from '@0xtrails/api'; const trails = new TrailsApi('YOUR_API_KEY'); // Token addresses (abbreviated - use full list in production) const TOKENS = { USDC: { 1: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Ethereum 8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base 42161: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' // Arbitrum } }; async function handlePaymentTool(params: { recipient_address: string; amount: string; token?: string; chain_id?: number; user_address: string; user_chain_id: number; user_token_address: string; }) { const { recipient_address, amount, token = 'USDC', chain_id = 8453, user_address, user_chain_id, user_token_address } = params; // Step 1: Get quote const { intent, gasFeeOptions } = await trails.quoteIntent({ ownerAddress: user_address, originChainId: user_chain_id, originTokenAddress: user_token_address, destinationChainId: chain_id, destinationTokenAddress: TOKENS[token]?.[chain_id], destinationToAddress: recipient_address, destinationTokenAmount: BigInt(parseFloat(amount) * 1e6).toString(), // Assuming 6 decimals tradeType: TradeType.EXACT_OUTPUT }); // Step 2: Return quote for user confirmation return { quote_id: intent.id, recipient: recipient_address, receive_amount: amount, receive_token: token, estimated_pay: intent.quote?.originTokenAmount, fees: { gas: gasFeeOptions?.feeOptions?.[0]?.fee, protocol: intent.quote?.fees }, expires_at: intent.expiresAt }; } // After user confirms, execute the quoted intent directly. // CommitIntent is deprecated — ExecuteIntent accepts QUOTED intents. async function executePayment(intent: any, signature: string) { const intentId = intent.intentId; const { intentStatus } = await trails.executeIntent({ intentId, depositSignature: { intentSignature: signature, deadline: Math.floor(Date.now() / 1000) + 3600 } }); // Stream status updates const receipt = await trails.waitIntentReceipt({ intentId }); return receipt; } ``` ## Error Handling Handle these common errors in your LLM integration: ```typescript theme={null} async function safePaymentCall(params: any) { try { return await handlePaymentTool(params); } catch (error: any) { const errorMap: Record = { // Quote errors 'INSUFFICIENT_BALANCE': 'User does not have enough tokens. Check their balance first.', 'NO_ROUTE_FOUND': 'No route available for this swap. Try a different token pair or chain.', 'AMOUNT_TOO_SMALL': 'Amount is below minimum. Minimum is typically $1 equivalent.', 'QUOTE_EXPIRED': 'Quote expired. Get a new quote before proceeding.', // Execution errors 'INTENT_EXPIRED': 'Intent expired before execution. Start over with a new quote.', 'DEPOSIT_FAILED': 'Token deposit failed. Check user approved the transaction.', 'SLIPPAGE_EXCEEDED': 'Price moved too much. Retry with higher slippage tolerance.', // Auth errors 'INVALID_API_KEY': 'API key invalid. Check configuration.', 'RATE_LIMITED': 'Too many requests. Wait before retrying.' }; const message = errorMap[error.code] || `Payment failed: ${error.message}`; return { success: false, error: error.code, user_message: message, retry_allowed: !['INVALID_API_KEY', 'AMOUNT_TOO_SMALL'].includes(error.code) }; } } ``` ### Error Response Format When returning errors to the LLM, use a structured format: ```json theme={null} { "success": false, "error": "NO_ROUTE_FOUND", "user_message": "I couldn't find a route to swap PEPE to USDC on Base. This token may not be supported or have insufficient liquidity. Would you like to try a different token?", "suggestions": [ "Try swapping to ETH first, then to USDC", "Check if the token is available on a different chain" ], "retry_allowed": true } ``` ## Best Practices Show the user the full quote including fees before calling `executeIntent`. Never auto-execute payments. Quotes expire in 5 minutes. If the user takes too long to confirm, get a fresh quote. Always validate Ethereum addresses and resolve ENS names before making API calls. Use `GetTokenList` to cache supported tokens rather than hardcoding. Token availability changes. When errors occur, give the LLM enough context to explain to the user and suggest alternatives. # Supported Chains Source: https://docs.trails.build/resources/supported-chains Complete list of supported blockchain networks, chain IDs, and token availability. ## Mainnet Chains Trails supports the following mainnet chains with full cross-chain routing capabilities: | Chain | Chain ID | Native Token | | - | - | - | | Ethereum | 1 | ETH | | Base | 8453 | ETH | | Arbitrum | 42161 | ETH | | Optimism | 10 | ETH | | Polygon | 137 | POL | | Avalanche | 43114 | AVAX | | BNB Chain | 56 | BNB | | Gnosis | 100 | xDAI | | Arbitrum Nova | 42170 | ETH | | Ape Chain | 33139 | APE | | Soneium | 1868 | ETH | | Katana | 747474 | ETH | | Etherlink | 42793 | XTZ | | Berachain | 80094 | BERA | | Sonic | 146 | S | | Monad | 143 | MON | | Somnia | 5031 | SOMI | | HyperEVM | 999 | HYPE | | Arc | 5042 | USDC | | Robinhood Chain | 4663 | ETH | ## Edge Chains Trails supports **edge chains** — non-EVM ecosystems reached through edge rails that settle into any supported EVM chain and back. Every alternative-VM chain is an edge chain. These chains use their ecosystem-native chain IDs and address formats. | Chain | VM | Chain ID | Native Token | Status | | - | - | - | - | - | | Solana | SVM | 101 | SOL | ✅ Supported | | Tron | TVM | 728126428 | TRX | ✅ Supported | ## Stablecoin Details ### USDC Availability USDC is available on most major chains. Chains with CCTP support enable native USDC transfers without wrapped variants: | Chain | Symbol | USDC Contract Address | Decimals | CCTP | | - | - | - | - | - | | Ethereum | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | 6 | ✓ | | Base | USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | 6 | ✓ | | Arbitrum | USDC | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | 6 | ✓ | | Optimism | USDC | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | 6 | ✓ | | Polygon | USDC | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | 6 | ✓ | | Avalanche | USDC | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | 6 | ✓ | | Sonic | USDC | `0x29219dd400f2Bf60E5a23d13Be72B486D4038894` | 6 | ✓ | | Monad | USDC | `0x754704Bc059F8C67012fEd69BC8A327a5aafb603` | 6 | ✓ | | HyperEVM | USDC | `0xb88339CB7199b77E23DB6E890353E22632Ba630f` | 6 | ✓ | | Arc | USDC | `0x0000000000000000000000000000000000000000` | 18 | ✓ | | BNB Chain | USDC | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | 18 | - | | Gnosis | USDC | `0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83` | 6 | - | | Arbitrum Nova | USDC | `0x750ba8b76187092B0D1E87E28daaf484d1b5273b` | 6 | - | | Katana | USDC | `0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36` | 6 | - | | Etherlink | USDC | `0x796Ea11Fa2dD751eD01b53C372fFDB4AAa8f00F9` | 6 | - | | Berachain | USDC | `0x549943e04f40284185054145c6E4e9568C1D3241` | 6 | - | | Robinhood Chain | USDC | `0x80e0e24718dbFcad49ECAA6F1e6C89A190586cA8` | 6 | - | | Soneium | USDC.e | `0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369` | 6 | - | | Somnia | USDC.e | `0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00` | 6 | - | | Ape Chain | USDC.e | `0xF1815bd50389c46847f0Bda824eC8da914045D14` | 6 | - | | Solana | USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | 6 | - | ### USDT Availability | Chain | Symbol | USDT Contract Address | Decimals | | - | - | - | - | | Ethereum | USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | 6 | | Base | USDT | `0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2` | 6 | | Arbitrum | USDT0 | `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9` | 6 | | Optimism | USDT | `0x94b008aA00579c1307B0EF2c499aD98a8ce58e58` | 6 | | Polygon | USDT | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` | 6 | | Avalanche | USDT.e | `0xc7198437980c041c805A1EDcbA50c1Ce5db95118` | 6 | | BNB Chain | USDT | `0x55d398326f99059fF775485246999027B3197955` | 18 | | Sonic | USDT | `0x6047828dc181963ba44974801FF68e538dA5eaF9` | 6 | | Katana | USDT | `0x2DCa96907fde857dd3D816880A0df407eeB2D2F2` | 6 | | Soneium | USDT | `0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35` | 6 | | Etherlink | USDT | `0x2C03058C8AFC06713be23e58D2febC8337dbfE6A` | 6 | | Berachain | USD₮0 | `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` | 6 | | HyperEVM | USD₮0 | `0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb` | 6 | | Solana | USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 | | Tron | USDT | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | Addresses on **Solana** are base58 SPL mint addresses and on **Tron** are base58 TRC-20 contract addresses, not EVM contract addresses. Solana and Tron route via edge rails rather than CCTP. On **Arc**, USDC is the native gas token (18 decimals). The 6-decimal ERC-20 at `0x3600000000000000000000000000000000000000` is also USDC, but Trails expects `0x0000000000000000000000000000000000000000` for Arc USDC. ## Supported Bridges and Liquidity Trails routes transactions through the optimal liquidity source for each transfer: | Provider | ID | Type | Description | | - | - | - | - | | **Relay** | `RELAY` | Bridge / Filler | Fast cross-chain execution via intent-based filler network | | **CCTP V2** | `CCTP` | Bridge | Native USDC cross-chain transfers via Circle's burn-and-mint | | **Hyperlane** | `HYPERLANE` | Bridge | Warp routes over Hyperlane interchain messaging | | **LayerZero OFT** | `LZ_OFT` | Bridge | Omnichain Fungible Token and Stargate pool routes | | **LayerZero Transfer** | `LZ_TRANSFER` | Bridge | LayerZero Value Transfer API; explicit selection only | | **Open Intents Framework** | `OIF` | Bridge | Solver-filled cross-chain intents; explicit selection only | | **Gas.zip** | `GASZIP` | Bridge | Gas-optimized cross-chain routing | | **LI.FI** | `LIFI` | Bridge / DEX aggregator | Aggregated cross-chain and same-chain routing | | **0x Protocol** | `ZEROX` | DEX aggregator | Best execution routing across multiple DEXs | | **SushiSwap** | `SUSHI` | DEX | On-chain swap routing with V3 concentrated liquidity | | **Somnia Exchange** | `SOMNIA_EXCHANGE` | DEX | Multi-hop swap routes on Somnia | | **Somnia Swap** | `SOMNIA_SWAP` | DEX | USDso and USDC.e swaps on Somnia | | **Everlong** | `EVERLONG` | DEX | Same-chain swaps on Katana | | **WETH** | `WETH` | Wrapper | Native token wrapping and unwrapping on the same chain | See [Route Providers](/sdk/quote-providers) for details on configuring bridge and swap providers. ## Request a New Chain Need a chain that's not listed? [Contact us](https://t.me/build_with_trails) to request integration. ## Related Resources GetChains API endpoint Configure bridge and swap providers # v1.5 Migration Guide Source: https://docs.trails.build/resources/v1-5-migration How to migrate from earlier versions of Trails to SDK 0.15.0 and intent protocol v1.5. ## Highlights * Trails v1.5 intent protocol is now live in production with many improvements to the intent protocol from gas savings, flexibility, 1-click gasless flows, and expanding chain routes. * The Trails API supports both v1 (original) and v1.5 (new) intent protocols out-of-the-box and apps using older Trails SDK versions will default to using the v1 intent protocol as before. * We recommend apps to upgrade to the latest `0xtrails@0.15.0` sdk to take advantage of many upgrades and the new `v1.5` intent protocol. * Please review the upgrade / migration notes below in detail. ## Upgrade / Migration Notes This guide covers the breaking changes in **SDK 0.15.0** and **intent protocol v1.5**, and how to update your integration. **`useQuote` amount units changed.** `from.amount` and `to.amount` are now human-readable decimal strings (e.g. `"1.5"`), not raw smallest-unit values. Internally they are scaled via `parseUnits` using the token's decimals. Passing a wei-style string like `"1500000000000000000"` will quote a vastly larger trade than intended — audit every `useQuote` call site before upgrading. To pass raw values, use `from.amountRaw` or `to.amountRaw` instead. ## Summary checklist Replace flat props (`fromTokenAddress`, `fromChainId`, etc.) with `from: { token, chain, amount }` and `to: { token, chain, recipient, ... }`. Change `from.amount` / `to.amount` in `useQuote` and `tokenAmount` / `fromAmount` in `useTrailsSendTransaction` from raw wei to human-readable decimal strings or use the raw value amount props. Delete the `paymasterUrls` prop from any `TrailsWidget` usage. Rename all `onCheckout*` callbacks to the new names (see table above). Replace `to.calldata` on `useQuote` with `actions` (for supported protocols) or `to.calls` (for custom contracts). ## Breaking changes ### `useQuote` amount units changed NOTE: `useQuote` hook is an optional low-level integration some developers use. If you're using Trails with just the widget, this secton does not apply to you. If you are using `useQuote` please please the notes and upgrade. `from.amount` and `to.amount` are now **human-readable decimal strings** (e.g. `"1.5"`), not raw smallest-unit integers. They are scaled internally using `parseUnits` with the token's decimals. Use `from.amountRaw` or `to.amountRaw` for passing raw values. ```tsx theme={null} // Before — raw wei from: { token: 'USDC', chain: 'arbitrum', amount: '1000000' } // 1 USDC // After — human-readable from: { token: 'USDC', chain: 'arbitrum', amount: '1' } // 1 USDC ``` If you need to pass a raw value (bigint or string), use `from.amountRaw` or `to.amountRaw` instead of `from.amount` / `to.amount`. The same applies to `useTrailsSendTransaction`: `tokenAmount` and `fromAmount` are now human-readable decimal strings. Native `value` remains raw wei bigint (wagmi-compatible). Use `tokenAmountRaw` or `fromAmountRaw` to pass raw values. You can also pass `tokenDecimals` / `fromDecimals` overrides to skip the registry or on-chain decimal lookup. *** ### `useQuote` restructured inputs `useQuote` now takes structured `from` and `to` objects instead of flat props. The flat fields — `fromTokenAddress`, `fromChainId`, `toTokenAddress`, `toChainId`, `toAddress`, `toApprove`, `toCalldata`, `swapAmount`, `tradeType` — are all gone. **Trade type is now inferred:** set `from.amount` for exact-input, `to.amount` for exact-output. ```tsx theme={null} // Before const { quote, send } = useQuote({ fromTokenAddress: '0xA0b8...', // USDC fromChainId: 42161, toTokenAddress: '0x8335...', // USDC on Base toChainId: 8453, toAddress: address, swapAmount: '1000000', // raw wei tradeType: 'EXACT_INPUT', }) // After const { quote, send } = useQuote({ from: { token: 'USDC', chain: 'arbitrum', amountRaw: '1000000' }, // or use "from.amount" which accepts human readable amount string ("1" = 1 USDC) to: { token: 'USDC', chain: 'base', recipient: address }, }) ``` *** ### `useQuote to.calldata` deprecated Passing `to.calldata` on `useQuote` now logs a runtime deprecation warning and is mutually exclusive with `actions` and `to.calls`. Migrate to one of: * **`actions`** — composable DeFi primitives (`lend`, `deposit`, `swap`, etc.) * **`to.calls`** — raw ABI-encoded `Call[]` for custom contracts ```tsx theme={null} // Deprecated const { send } = useQuote({ from: { ... }, to: { ..., calldata: encodedCalldata }, }) // Preferred — composable actions const { send } = useQuote({ from: { ... }, to: { ... }, actions: [lend({ marketId: 'base-usdc-aave-v3-lending', amount: '100' })], }) ``` ### `paymasterUrls` removed from `TrailsWidget` The `paymasterUrls` prop has been removed from `TrailsWidget`. Remove it from your configuration — it has no replacement. ```tsx theme={null} // Before // After ``` *** ### Widget lifecycle callback renames All `TrailsWidget` checkout lifecycle callbacks were renamed. The `onCheckout` prefix is gone. | Before (0.13.x) | After (0.15.0) | | - | - | | `onCheckoutStart` | `onStart` | | `onCheckoutQuote` | `onQuote` | | `onCheckoutSignatureRequest` | `onSignRequest` | | `onCheckoutSignatureConfirmed` | `onSign` | | `onCheckoutSignatureRejected` | `onSignReject` | | `onCheckoutComplete` | `onSuccess` | | `onCheckoutError` | `onError` | | `onCheckoutStatusUpdate` | `onStatus` | | `onCheckoutApprovalRequest` | `onApproveRequest` | | `onCheckoutApprovalConfirmed` | `onApprove` | | `onCheckoutApprovalRejected` | `onApproveReject` | ```tsx theme={null} // Before // After ``` *** ## New features ### Composable actions `useQuote` gains an `actions` array for composing destination-side DeFi flows: All amount props for actions are in human-readable form only. ```tsx theme={null} import { useQuote, swap, lend, deposit, dynamic, assertCondition } from '0xtrails' const { send } = useQuote({ from: { chain: 'arbitrum', token: 'USDC' }, to: { chain: 'polygon', token: 'USDT', amount: '0.2' }, actions: [ deposit({ marketId: 'polygon-usdt-bbqusdt0-0xb7c9988d...-4626-vault', amount: '0.1', }), swap({ tokenIn: 'USDT', tokenOut: 'USDC', amountIn: dynamic(), // resolved at runtime from wallet balance fee: '0.3', }), assertCondition({ erc20Balance: { token: 'USDC', gte: '0.08' }, }), lend({ marketId: 'polygon-usdc-aave-v3-lending', amount: dynamic(), }), ], }) ``` **Action factories:** `swap`, `lend`, `deposit`, `custom`, `assertCondition` * Use `lend` for lending markets (Aave), `deposit` for ERC-4626 vaults (Morpho, Yearn). * Discover `marketId` values with `useEarnMarkets` filtered by `type: "lending"` or `type: "vault"`. **Runtime sentinels:** * `dynamic()` — resolves to the intent wallet's ERC-20 balance at execution time * `self()` — resolves to `address(this)` (the intent wallet under `DELEGATECALL`) **Low-level escape hatch:** `useQuote({ to: { calls } })` accepts raw `Call[]` (or `Call[][]`) for hand-rolled ABI-encoded transactions. Calls support `dynamicAmountToken`, `hydrateEntries`, and `sweepTokens` for runtime hydration and dust recovery. *** ### New hooks | Hook | Purpose | | - | - | | `useEarnMarkets` | Discover lending and vault markets usable with `lend` / `deposit` | | `useEarnBalances` | Read a user's balances across earn markets | | `useEarnProviders` | List supported earn protocols | | `useResolveActions` | Resolve `ActionItem[]` into encoded destination `Call[]` | *** ### Mode-specific widget components New typed wrappers exported from `0xtrails/widget`, each with a tighter prop shape than the generic `TrailsWidget`: ```tsx theme={null} import { Pay, Fund, Swap, Earn, Withdraw } from '0xtrails/widget' ``` | Component | Use case | | - | - | | `Pay` | Payments to a recipient — structured `to`/`from`, `paymentMethod` discriminator | | `Fund` | Fund a wallet from any source | | `Swap` | Swap-only flows | | `Earn` | Deposit and withdraw from earn markets | | `Withdraw` | Withdrawal flows | `TrailsWidget` with `mode="pay" | "fund" | "swap" | "earn" | "withdraw"` continues to work. These wrappers are additive. *** ### Other additions * **`intentProtocolVersion` prop** on `TrailsWidget` — pin the intent protocol version per-instance. * **`fromAmount` prop** on `TrailsWidget` — pre-populate source amount in human-readable units (e.g. `"1.5"`). * **`fundOptions` additions** on `TrailsWidget` — new `paymentMethodType` field (Meld payment method pre-selection, e.g. `"CREDIT_DEBIT_CARD"`) and `meshExchangeKey` field (Mesh exchange pre-selection, e.g. `"coinbase"`). * **Wallet-less quote preview** — pass `walletAddress` to `useQuote` before a wallet is connected. * **Auto-refresh expired quotes (widget only)** — The Trails Widget now schedule a refetch when a quote's expiresAt passes (waking on the next user interaction), so the "Intent quote has expired" pre-signing error no longer appears in the built-in UI. * **New utilities:** `erc20Utils`, `buildCall`, `buildApproveAndCall`, `buildErc20Approve`, `getAmountWithSlippage`, `resolveChainId`, `resolveChainName`. * **API helpers:** `getEarnMarkets`, `getEarnBalances`, `getEarnProviders`. * **New error codes:** `CHAIN_MISMATCH`, `CALL_RESOLUTION_FAILED`. * **`0xtrails/hydrate` subpath** — Hydrate builders, selectors, and calldata helpers for v1.5 execution flows. # Using Dynamic Source: https://docs.trails.build/sdk/adapters/dynamic Use Dynamic embedded wallets (Fireblocks) with Trails — connect via Dynamic, sync into wagmi, and pass wagmiAdapter to the widget. [Dynamic](https://www.dynamic.xyz/) powers Fireblocks embedded wallets on web. Dynamic does not need a dedicated Trails adapter — it plugs into wagmi via [`@dynamic-labs/wagmi-connector`](https://www.dynamic.xyz/docs/react/reference/using-wagmi), and Trails shares that session through the standard [`wagmiAdapter`](/sdk/adapters/wagmi). No custom hook is required. Once the user connects with Dynamic's embedded wallet, the Trails widget reads the same wagmi connection state. The division of labor: * **Dynamic owns the wallet**: embedded wallet creation, auth, recovery, and connect UI (`DynamicWidget`). * **Trails owns the payment**: routing, quotes, and cross-chain execution, signed through the bridged wagmi session. ## Install ```bash theme={null} pnpm add 0xtrails @0xtrails/adapter-wagmi \ @dynamic-labs/sdk-react-core @dynamic-labs/ethereum @dynamic-labs/wagmi-connector \ wagmi viem @tanstack/react-query ``` Dynamic documents wagmi compatibility up to **v3.1.0**. If you hit connector issues, pin wagmi to a version Dynamic supports and test before upgrading. ## 1. Configure Dynamic + wagmi Create the wagmi config at **module scope** (not inside a component). Set `multiInjectedProviderDiscovery: false` — Dynamic handles injected provider discovery itself. ```tsx theme={null} // wagmi.config.ts import { createConfig, http } from 'wagmi' import { base, mainnet } from 'viem/chains' export const wagmiConfig = createConfig({ chains: [base, mainnet], multiInjectedProviderDiscovery: false, transports: { [base.id]: http(), [mainnet.id]: http(), }, }) ``` Include every chain you enable in the Dynamic dashboard. Dynamic no longer auto-syncs chains into your wagmi config — you declare them explicitly in both places. ## 2. Wrap the app `DynamicWagmiConnector` keeps Dynamic's active wallet in sync with wagmi. The user connects through `DynamicWidget` before using the Trails widget. ```tsx theme={null} // App.tsx import { DynamicContextProvider, DynamicWidget } from '@dynamic-labs/sdk-react-core' import { EthereumWalletConnectors } from '@dynamic-labs/ethereum' import { DynamicWagmiConnector } from '@dynamic-labs/wagmi-connector' import { WagmiProvider } from 'wagmi' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { wagmiConfig } from './wagmi.config' import { Checkout } from './Checkout' const queryClient = new QueryClient() export function App() { return ( ) } ``` ## 3. Pass wagmiAdapter to the widget Define adapters once at module scope. Pass the same `wagmiConfig` instance you use in `WagmiProvider`, or call `wagmiAdapter()` with no options to auto-bridge into the surrounding provider. ```tsx theme={null} // Checkout.tsx import { Fund } from '0xtrails/widget' import { wagmiAdapter } from '@0xtrails/adapter-wagmi' import { useAccount } from 'wagmi' import { wagmiConfig } from './wagmi.config' const adapters = [wagmiAdapter({ wagmiConfig })] export function Checkout() { const { isConnected } = useAccount() if (!isConnected) { return

Connect with Dynamic first.

} return ( ) } ``` That's the full integration — Dynamic connects the embedded wallet, wagmi holds the session, and Trails signs through `wagmiAdapter`. ## Tips * **Keep config stable** — define `wagmiConfig` and `adapters` at module scope (or memoize them). Recreating them on every render remounts the wallet runtime. * **Connect before the widget** — the Trails widget expects an active wagmi session. Use `DynamicWidget` (or your own Dynamic login flow) first. * **Match chains** — chains in `wagmiConfig`, the Dynamic dashboard, and your Trails destination chain should align. # EIP-1193 Adapter Source: https://docs.trails.build/sdk/adapters/eip-1193 Connect any EIP-1193 provider to Trails with the evmAdapter helper — browser wallets, embedded wallets, or any SDK that exposes a standard Ethereum provider. `evmAdapter` is the universal EVM adapter, exported directly from `0xtrails` — no extra package needed. It accepts anything that implements the [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) provider interface: `window.ethereum`, embedded wallet providers, or providers produced by wallet SDKs. If a wallet library can hand you an object with a `request({ method, params })` method, you can plug it into Trails with `evmAdapter`. ## Quick start ```tsx theme={null} import { Swap, evmAdapter } from '0xtrails' const adapters = [ evmAdapter({ wallets: { id: 'browser-eip1193', name: 'Browser Wallet', provider: () => window.ethereum ?? null, }, }), ] ``` The provider can be passed as the object itself, or as a (sync or async) function returning it — useful when the provider isn't available at module load time. ## Options ```ts theme={null} evmAdapter({ wallets, // one wallet descriptor or an array chains, // optional: defaults to all Trails-supported chains supportsWalletSelection, // optional: defaults to true }) ``` ### Wallet descriptor | Field | Type | Description | | - | - | - | | `id` | `string` | Stable identifier for the wallet | | `name` | `string` | Display name in the Trails UI | | `icon` | `string` | Optional icon URL | | `provider` | `Eip1193ProviderLike \| () => provider \| Promise` | The EIP-1193 provider, or a factory returning it (or `null` when unavailable) | | `showDisconnect` | `boolean` | Whether Trails shows a disconnect action for this wallet. Defaults to `true` | The adapter subscribes to the provider's `accountsChanged`, `chainChanged`, and `disconnect` events, so account and network switches in the wallet are reflected in Trails automatically. Chain switching uses `wallet_switchEthereumChain`, adding the chain via `wallet_addEthereumChain` when needed. Viem chain objects are compatible with this shape. ## Host-owned wallets When your app owns the wallet session — an embedded wallet, or a wallet SDK with its own connect UI — tell Trails to use the wallet without showing its own wallet-management surfaces: ```tsx theme={null} const adapters = [ evmAdapter({ wallets: { id: 'embedded-wallet', name: 'My App Wallet', provider: embeddedProvider, showDisconnect: false, // your app owns disconnect }, supportsWalletSelection: false, // hide Trails's wallet picker }), ] ``` This is exactly the pattern used to integrate wallet SDKs like [thirdweb](/sdk/adapters/thirdweb) ## Multiple wallets Pass an array to offer several EIP-1193 wallets in the Trails connect UI: ```tsx theme={null} const adapters = [ evmAdapter({ wallets: [ { id: 'wallet-a', name: 'Wallet A', provider: providerA }, { id: 'wallet-b', name: 'Wallet B', provider: () => getProviderB() }, ], }), ] ``` # Adapters Overview Source: https://docs.trails.build/sdk/adapters/overview Wallet adapters let Trails plug into any wallet stack — wagmi, raw EIP-1193 providers, Solana wallets, or embedded wallets like Thirdweb, Privy, and Fireblocks. The Trails Widget is not tied to any particular wallet library. The SDK talks to wallets through **adapters**: small factories that bridge an existing wallet runtime (wagmi, an EIP-1193 provider, a Solana wallet, an embedded wallet SDK) into the Trails widget. Omit adapters entirely and Trails uses its **built-in EVM wallet runtime**, your app wallet context stays separate. Pass adapters when your app already owns wallet state and Trails should share it instead of managing its own. ## Available adapters | Adapter | Package | Use it when | | - | - | - | | [`wagmiAdapter`](/sdk/adapters/wagmi) | `@0xtrails/adapter-wagmi` | Your app uses wagmi (v2 or v3), or a wagmi-based stack such as [Dynamic](/sdk/adapters/dynamic), Privy, or Sequence Connect | | [`evmAdapter`](/sdk/adapters/eip-1193) | `0xtrails` (built in) | You have a raw EIP-1193 provider — `window.ethereum`, an embedded wallet, or any SDK that can expose one | | [`svmAdapter`](/sdk/adapters/solana) | `@0xtrails/svm` | You want to accept or send payments from Solana wallets | | [`tronAdapter`](/sdk/adapters/tron) | `@0xtrails/tvm` | You want to accept or send payments from Tron wallets. Requires a [`Buffer` polyfill](#buffer-polyfill-for-tron) | Wallet SDKs that can produce an EIP-1193 provider don't need a dedicated adapter — they plug into `evmAdapter` directly. See the [thirdweb](/sdk/adapters/thirdweb) guide for a complete example of this pattern. ## Where to pass adapters Adapters are a **widget-only** feature. Pass them on focused widget components such as `Pay`, `Fund`, `Swap`, `Earn`, `Withdraw`. ```tsx theme={null} import { Fund } from '0xtrails/widget' import { wagmiAdapter } from '@0xtrails/adapter-wagmi' const adapters = [wagmiAdapter()] ``` ## Mixing adapters You can pass several adapters together. EVM adapters compose into one wallet runtime; Solana and Tron adapters stack alongside, so EVM, Solana, and Tron wallets work in the same widget: ```tsx theme={null} import { evmAdapter } from '0xtrails' import { svmAdapter } from '@0xtrails/svm' import { tronAdapter } from '@0xtrails/tvm' const adapters = [ evmAdapter({ wallets: { id: 'browser', name: 'Browser Wallet', provider: () => window.ethereum ?? null }, }), svmAdapter({ rpcUrls: ['https://api.mainnet-beta.solana.com'] }), tronAdapter(), ] ``` ## Buffer polyfill for Tron `tronAdapter` pulls in TronWeb, which reads the Node `Buffer` global while its modules evaluate. Browsers don't define `Buffer`, so without a polyfill the import throws `ReferenceError: Buffer is not defined` and takes down the surrounding React tree — typically a blank page rather than a caught error. Install the `buffer` package and assign the global before anything imports the adapter: ```bash theme={null} pnpm add buffer ``` ```ts theme={null} // polyfills.ts import { Buffer } from 'buffer' if (typeof globalThis.Buffer === 'undefined') { globalThis.Buffer = Buffer } ``` Import it as the first import of the entry that loads `tronAdapter`: ```tsx theme={null} import './polyfills' import { Pay } from '0xtrails' import { tronAdapter } from '@0xtrails/tvm' ``` Import order matters — the assignment has to run before TronWeb's modules evaluate, so keep it above the adapter import. Bundlers that hoist or split entries can reorder this; if the error persists, inject the global at build time instead (for example Vite's `define`, or `webpack.ProvidePlugin`). `svmAdapter` does not need this — Solana support runs in the browser without a `Buffer` global. ## Choosing an adapter 1. **No existing wallet stack?** Pass no adapters — the built-in runtime handles injected wallets and WalletConnect. 2. **App on wagmi?** Use [`wagmiAdapter`](/sdk/adapters/wagmi). One package supports wagmi 2 and wagmi 3. Wagmi-based wallet SDKs such as [Dynamic](/sdk/adapters/dynamic) (Fireblocks embedded wallets) plug in the same way — no custom Trails code needed. 3. **Embedded or custom wallet that speaks EIP-1193?** Use [`evmAdapter`](/sdk/adapters/eip-1193). This covers wallet SDKs that expose an EIP-1193 provider directly, such as [thirdweb](/sdk/adapters/thirdweb). 4. **Solana?** Add [`svmAdapter`](/sdk/adapters/solana). 5. **Tron?** Add [`tronAdapter`](/sdk/adapters/tron), plus the [`Buffer` polyfill](#buffer-polyfill-for-tron). # Solana Adapter Source: https://docs.trails.build/sdk/adapters/solana Accept or send payments from Solana wallets with the svmAdapter from @0xtrails/svm, auto-detect Wallet Standard wallets or bring your own Solana wallet stack. `svmAdapter` from `@0xtrails/svm` adds Solana (SVM) support to the Trails widget. It registers the SVM edge plugin and provides the Solana wallet context when passed through `adapters`. One adapter covers every integration style: built-in Wallet Standard auto-detection, the modern `@solana/react-hooks` stack, or the legacy `@solana/wallet-adapter-react` stack. ## Install ```bash theme={null} pnpm add 0xtrails @0xtrails/svm ``` ## Auto-detection (no Solana provider needed) The simplest setup — the adapter discovers [Wallet Standard](https://github.com/wallet-standard/wallet-standard) wallets (Phantom, Solflare, Backpack, …) on its own. No app-level Solana wallet provider is required: ```tsx theme={null} import { Fund } from '0xtrails' import { svmAdapter } from '@0xtrails/svm' ``` Auto-detection options: | Option | Type | Description | | - | - | - | | `rpcUrls` | `string[]` | RPC endpoints used by the built-in detection flow, falls back in the provided order | | `preferredWalletId` | `string` | Wallet Standard wallet id to select/connect first | | `silentReconnect` | `boolean` | Silently reconnect previously trusted wallets. Defaults to `true` | ## Bring your own wallet (`@solana/react-hooks`) If your app already manages Solana wallet state, pass the wallet handle directly. With the modern `@solana/react-hooks` stack this is a one-liner: ```tsx theme={null} import { autoDiscover, createClient } from '@solana/client' import { SolanaProvider, useWalletConnection } from '@solana/react-hooks' import { Pay } from '0xtrails' import { svmAdapter } from '@0xtrails/svm' const solanaClient = createClient({ endpoint: SOLANA_RPC_URL, walletConnectors: autoDiscover(), }) function Checkout() { const { wallet } = useWalletConnection() return ( ) } export function App() { return ( ) } ``` `wallet` accepts Wallet Standard wallets, standard-compatible wallet adapters, and Kit-style wallet sessions. ## Legacy `@solana/wallet-adapter-react` The legacy Solana Wallet Adapter stack works too — adapt its `@solana/web3.js` transaction signing into the serialized signer shape `svmAdapter` expects: ```tsx theme={null} import { Fund } from '0xtrails' import { svmAdapter } from '@0xtrails/svm' import { useConnection, useWallet } from '@solana/wallet-adapter-react' import { Transaction, VersionedTransaction } from '@solana/web3.js' import { useMemo } from 'react' function deserializeTransaction(bytes: Uint8Array) { try { return VersionedTransaction.deserialize(bytes) } catch { return Transaction.from(bytes) } } function Checkout() { const { connection } = useConnection() const { connected, publicKey, wallet, signTransaction } = useWallet() const solanaWallet = useMemo( () => ({ connected, publicKey, walletName: wallet?.adapter.name ?? 'Solana Wallet', walletIcon: wallet?.adapter.icon, connection: { getLatestBlockhash: (commitment?: string) => connection.getLatestBlockhash(commitment as never), sendRawTransaction: (transaction: unknown) => connection.sendRawTransaction(transaction as Uint8Array), }, signSerializedTransaction: async (bytes: Uint8Array) => { if (!signTransaction) { throw new Error('Connected Solana wallet cannot sign transactions') } const signed = await signTransaction(deserializeTransaction(bytes)) return signed.serialize() }, }), [connected, connection, publicKey, signTransaction, wallet], ) return ( ) } ``` ## Options reference ```ts theme={null} svmAdapter({ // Bring-your-own-wallet mode wallet, // Wallet Standard wallet, wallet adapter, or Kit wallet session account, // optional explicit Wallet Standard account (defaults to first Solana account) connection, // optional RPC connection override connected, // optional explicit connected flag chain, // optional chain identifier // Auto-detection mode rpcUrls, // RPC endpoints for built-in Wallet Standard detection preferredWalletId, // preferred wallet to select/connect first silentReconnect, // silently reconnect trusted wallets (default: true) }) ``` # Using thirdweb Source: https://docs.trails.build/sdk/adapters/thirdweb Use thirdweb's Connect SDK with Trails — convert the active thirdweb wallet to an EIP-1193 provider and hand it to evmAdapter. thirdweb doesn't need a dedicated Trails adapter package. Its Connect SDK can convert any active wallet — injected, in-app/social login, or smart account — into a standard EIP-1193 provider with `EIP1193.toProvider`, which plugs straight into Trails's built-in [`evmAdapter`](/sdk/adapters/eip-1193). The division of labor: * **thirdweb owns the wallet**: connection UI (`ConnectButton`), session persistence, social login, smart accounts. * **Trails owns the payment**: routing, quotes, and cross-chain execution, signed through the provider it was handed. ## Install ```bash theme={null} pnpm add 0xtrails thirdweb @tanstack/react-query viem ``` ## 1. Create the thirdweb client ```tsx theme={null} // thirdwebClient.ts import { createThirdwebClient } from 'thirdweb' import { base } from 'thirdweb/chains' export const thirdwebClient = createThirdwebClient({ clientId: 'YOUR_THIRDWEB_CLIENT_ID', }) export const defaultChain = base ``` ## 2. Bridge the active thirdweb wallet to a Trails adapter This hook is the entire integration. It reads the live wallet state with thirdweb's hooks, converts the wallet to EIP-1193, and wraps it with `evmAdapter`: ```tsx theme={null} // useThirdwebTrailsAdapters.ts import { evmAdapter, type Eip1193ProviderLike } from '0xtrails' import { useMemo } from 'react' import { useActiveAccount, useActiveWallet, useActiveWalletChain, } from 'thirdweb/react' import { EIP1193 } from 'thirdweb/wallets' import { defaultChain, thirdwebClient } from './thirdwebClient' export function useThirdwebTrailsAdapters() { const activeAccount = useActiveAccount() const activeWallet = useActiveWallet() const activeChain = useActiveWalletChain() return useMemo(() => { if (!activeAccount || !activeWallet) { return undefined } // thirdweb wallet (injected, in-app, smart account, ...) → EIP-1193 const provider = EIP1193.toProvider({ wallet: activeWallet, chain: activeChain ?? defaultChain, client: thirdwebClient, }) return [ evmAdapter({ wallets: [{ id: 'thirdweb-active-wallet', name: 'thirdweb', provider: provider as Eip1193ProviderLike, showDisconnect: false, // thirdweb owns disconnect }], supportsWalletSelection: false, // hide Trails's wallet picker }), ] as const }, [activeAccount, activeWallet, activeChain]) } ``` Two details matter here: * `showDisconnect: false` and `supportsWalletSelection: false` mark the wallet as **host-owned**: Trails uses it for signing but never shows its own connect, disconnect, or wallet-picker UI — thirdweb's `ConnectButton` owns that lifecycle. * The `useMemo` dependencies mean the adapter rebuilds whenever the user switches accounts or chains in thirdweb, so Trails always tracks the live wallet. ## 3. Wire it up thirdweb handles connection; the widget renders once a wallet is active: ```tsx theme={null} // App.tsx import { Fund } from '0xtrails' import { ConnectButton, ThirdwebProvider } from 'thirdweb/react' import { thirdwebClient } from './thirdwebClient' import { useThirdwebTrailsAdapters } from './useThirdwebTrailsAdapters' function Checkout() { const adapters = useThirdwebTrailsAdapters() if (!adapters) { return

Connect a wallet first.

} return ( ) } export function App() { return ( ) } ``` ## The pattern generalizes Nothing here is thirdweb-specific from Trails's point of view: any wallet SDK that can expose an EIP-1193 provider integrates the same way — convert to a provider, wrap with `evmAdapter`, mark it host-owned. See the [EIP-1193 adapter](/sdk/adapters/eip-1193) page for the full `evmAdapter` API. # Tron Adapter Source: https://docs.trails.build/sdk/adapters/tron Accept or send payments from Tron wallets with the tronAdapter from @0xtrails/tvm, auto-detect TronLink and other TVM wallets or bring your own Tron wallet stack. `tronAdapter` from `@0xtrails/tvm` adds Tron (TVM) support to the Trails widget. It registers the TVM edge plugin and provides the Tron wallet context when passed through `adapters`. Two integration styles are supported: built-in Tron wallet auto-detection (TronLink, OKX, TokenPocket, Trust, MetaMask on Tron, and WalletConnect), or bring your own Tron wallet. ## Install ```bash theme={null} pnpm add 0xtrails @0xtrails/tvm ``` `@0xtrails/tvm` requires a `Buffer` global in the browser. Set up the [polyfill](/sdk/adapters/overview#buffer-polyfill-for-tron) before importing the adapter, or the page renders blank. ## Auto-detection (no Tron provider needed) The simplest setup — the adapter discovers Tron wallets on its own. No app-level Tron wallet provider is required: ```tsx theme={null} import { Fund } from '0xtrails' import { tronAdapter } from '@0xtrails/tvm' ``` Auto-detection options: | Option | Type | Description | | - | - | - | | `preferredWalletId` | `string` | Wallet id to select/connect first (matched against adapter name) | | `silentReconnect` | `boolean` | Silently reconnect previously trusted wallets. Defaults to `true` | ## Bring your own wallet Pass a `wallet` handle if your app already manages Tron wallet state: ```tsx theme={null} import { Fund } from '0xtrails' import { tronAdapter } from '@0xtrails/tvm' function Checkout() { const wallet = /* your Tron wallet, see @0xtrails/tvm's CreateTronWalletInput */ return ( ) } ``` ## Tron RPC The SDK reads Tron balances from TronGrid at `https://api.trongrid.io`. Public TronGrid is rate-limited, so pass your own endpoint or API key in production, either on the widget or on [`TrailsProvider`](/sdk/hooks#configuration-options): ```tsx theme={null} ``` | Prop | Type | Description | | - | - | - | | `tronRpcUrl` | `string` | Base URL of a TronGrid-compatible HTTP API | | `tronApiKey` | `string` | Sent as the `TRON-PRO-API-KEY` header | Widget props take precedence over the `TrailsProvider` config. ## Compose with other adapters EVM, Solana, and Tron adapters compose — pass any combination and every wallet family works in the same widget: ```tsx theme={null} import { evmAdapter } from '0xtrails' import { svmAdapter } from '@0xtrails/svm' import { tronAdapter } from '@0xtrails/tvm' const adapters = [ evmAdapter({ wallets: { id: 'browser', name: 'Browser Wallet', provider: () => window.ethereum ?? null }, }), svmAdapter({ rpcUrls: ['https://api.mainnet-beta.solana.com'] }), tronAdapter(), ] ``` ## Options reference ```ts theme={null} tronAdapter({ // Bring-your-own-wallet mode wallet, // Tron wallet handle (CreateTronWalletInput) // Auto-detection mode preferredWalletId, // preferred wallet to select/connect first silentReconnect, // silently reconnect trusted wallets (default: true) }) ``` Tron address formats and TRC-20 token addresses use Tron's base58 encoding — see the [Supported Chains → Edge Chains](/resources/supported-chains#edge-chains) table for representative Tron token addresses. # wagmi Adapter Source: https://docs.trails.build/sdk/adapters/wagmi Share your app's existing wagmi session with Trails using @0xtrails/adapter-wagmi. Supports wagmi 2 and wagmi 3. `@0xtrails/adapter-wagmi` lets Trails use the wagmi setup your app already has. Use it when your app owns `WagmiProvider`, chains, transports, and connectors, and you want the Trails widget to share that wallet state. A single package covers both major wagmi versions: `wagmi@^2.19.0 || ^3.2.0`. Wagmi-based stacks such as [Dynamic](/sdk/adapters/dynamic) (Fireblocks embedded wallets), [Privy](#embedded-wallets-privy), and Sequence Connect work through this adapter too. ## Install ```bash theme={null} pnpm add @0xtrails/adapter-wagmi ``` ## Quick start The adapter auto-detects a surrounding `WagmiProvider` ```tsx theme={null} import { Fund } from '0xtrails' import { wagmiAdapter } from '@0xtrails/adapter-wagmi' import { WagmiProvider } from 'wagmi' const adapters = [wagmiAdapter()] export function App() { return ( ) } ``` ## Options ```ts theme={null} wagmiAdapter({ wagmiConfig, // optional: wagmi config for adapter-owned runtime fallback wallets, // optional: wallet descriptors for labels, ordering, custom connect flows sequence, // optional: host-owned Sequence Connect hooks }) ``` ### Custom wallet descriptors Pass `wallets` when runtime connector inference isn't enough — to control how a wallet appears in Trails, or to wire a custom connect flow: ```tsx theme={null} const adapters = [ wagmiAdapter({ wagmiConfig, wallets: [ { id: 'host-wallet', name: 'My Wallet', flow: 'direct', // or 'embedded' placement: 'featured', // pin to the top of the wallet list }, ], }), ] ``` Descriptors can also carry a `connect: () => Promise` callback that Trails invokes when the user picks that wallet — this is how embedded-wallet login flows are wired in (see the Privy example below). ## Embedded wallets (Privy) Privy exposes its embedded wallet through `@privy-io/wagmi`, so it works with this adapter. Build the wagmi config with Privy's `createConfig`, then describe the embedded wallet with a custom `connect` action that triggers Privy's login: ```tsx theme={null} import { wagmiAdapter } from '@0xtrails/adapter-wagmi' import { createConfig, WagmiProvider } from '@privy-io/wagmi' const wagmiConfig = createConfig({ chains, transports }) function usePrivyTrailsAdapters() { const connectPrivyEmbedded = usePrivyEmbeddedConnect() // your hook: connectOrCreateWallet + setActiveWallet return useMemo( () => [ wagmiAdapter({ wagmiConfig, wallets: [ { id: 'privy', name: 'Privy Embedded', flow: 'embedded', placement: 'featured', connect: connectPrivyEmbedded, }, ], }), ], [connectPrivyEmbedded], ) } ``` When the user selects "Privy Embedded" in the Trails connect UI, Trails calls your `connect` callback, Privy runs its login flow, and the resulting wagmi session is picked up automatically. ## Sequence Connect When wrapping the app in ``, pass the host-owned Sequence hooks so the adapter can expose Sequence's connect modal and WaaS fee options inside the widget: ```tsx theme={null} import { useOpenConnectModal, useWaasFeeOptions } from '@0xsequence/connect' import { wagmiAdapter } from '@0xtrails/adapter-wagmi' const adapters = [ wagmiAdapter({ wagmiConfig, sequence: { useOpenConnectModal, useWaasFeeOptions }, }), ] ``` # Changelog Source: https://docs.trails.build/sdk/changelog Release notes and migration notes for the 0xtrails SDK Track SDK releases, migration guidance, and notable changes. September 16, 2026 · Arc chain support (id 5042, USDC gas), and the `onramp.mesh.exchanges` allowlist now filters. September 10, 2026 · Connect screen rework — one flat wallet list, wallets connectable in any order, EVM owner requested just-in-time. September 1, 2026 · `consentCheckbox` prop gates the confirm button behind a terms tick. August 30, 2026 · Stripe onramp funding method, and a `labels` prop for overriding widget copy. August 18, 2026 · Wallet discovery fix — wallets that spoof a compatibility flag no longer claim the MetaMask slot. August 18, 2026 · Packaging fix; `@0xtrails/api@0.18.0` shipped without its build output. August 18, 2026 · Tron support via the new `@0xtrails/tvm` adapter, and a leaner send flow that skips `CommitIntent` — non-edge intents execute directly from `QUOTED` and edge intents arm through `ExecuteIntentEdge`. July 27, 2026 · Adds the `EVERLONG` route provider to the API bindings. July 21, 2026 · Solana support arrives — pay from and to Solana via edge rails with the new `@0xtrails/svm` adapter, plus gasless transactions and new bridge providers. May 21, 2026 · wagmi is now opt-in, fund flows support exact-output quoting, and quote handling supports passthrough quote, fee, and approval data. New release notes will be added here as SDK versions are published. # Building Actions Source: https://docs.trails.build/sdk/composable-actions/building-actions Full builder API: swap, lend, deposit, assertCondition, and custom ```ts theme={null} import { swap, lend, deposit, assertCondition, custom, dynamic, self, } from '0xtrails' ``` Amounts are generally one of: * **`string`** — human-readable, e.g. `"50"` or `"0.03"`. Scaled by the token's decimals. * **`bigint`** — raw wei, used as-is. * **`dynamic()`** — "use the intent wallet's runtime balance at execution time", accepted only on fields that spend a balance. See [Dynamic Values](/sdk/composable-actions/dynamic-values). ## `swap` — on-chain token swap Trade type is inferred from which amount you pass: `amountIn` → `EXACT_INPUT`, `amountOut` → `EXACT_OUTPUT`. Setting both (or neither) throws. `dynamic()` is allowed on `amountIn` for exact-input swaps and `maxAmountIn` for exact-output swaps. `amountOut` and `minAmountOut` must be concrete values because they are execution thresholds. ```ts theme={null} // EXACT_INPUT: spend 1 WETH, revert if output < 3000 USDC swap({ tokenIn: 'WETH', tokenOut: 'USDC', amountIn: '1', minAmountOut: '3000', fee: '0.05', // "0.01" | "0.05" | "0.3" (default) | "1" }) // EXACT_OUTPUT: receive exactly 3000 USDC, spend at most 2 WETH swap({ tokenIn: 'WETH', tokenOut: 'USDC', amountOut: '3000', maxAmountIn: '2', }) // Route through SushiSwap instead of Uniswap V3 swap({ provider: 'SUSHISWAP_V3', tokenIn: 'USDC', tokenOut: 'WETH', amountIn: '100', }) ``` **Defaults:** * `fee`: `"0.3"` (0.3% pool). * `provider`: `"UNISWAP_V3"`. If it isn't deployed on the destination chain, the error tells you to try SushiSwap. * `recipient`: the intent wallet for the final action; for intermediate actions, output flows back so the next action can consume it. Override only if you want to send output to a third party. ## Swap utilities For anything that isn't "build calldata for `useQuote`" — on-chain quoting, pool discovery, raw `Call[]` composition, address lookups — `0xtrails` exports the underlying chain-scoped helpers behind `swap`. The same adapter shape is exported for both Uniswap V3 and SushiSwap V3: `quoteSwap`, `getPool`, `swapExactInputSingle`, and `swapExactOutputSingle`. ```ts theme={null} import { uniswapV3, sushiswapV3 } from '0xtrails' ``` Low-level helpers take raw `bigint` wei — use viem's `parseUnits` to convert from human-readable amounts. Fee tiers take human strings: `"0.01"` / `"0.05"` / `"0.3"` / `"1"`. Typical use cases: * **Pre-flight quoting** — render expected output / price impact in the UI before the user commits, using `quoteSwap`. * **Pool liquidity check** — skip a pair or switch `fee` tier when `getPool` returns `null`. * **Custom compositions** — build `Call[]` directly and pass them through `to.calls` (`useQuote`) or `calls` (`useTrailsSendTransaction`) when you need something the typed `swap` doesn't cover (e.g. multi-hop paths, permit2, …). ### Chain-scoped adapter `uniswapV3.onChain(chain)` (or `sushiswapV3.onChain(chain)`) returns an adapter bound to a specific chain (name or chain ID): ```ts theme={null} import { uniswapV3 } from '0xtrails' const uni = uniswapV3.onChain('base') // or: uniswapV3.onChain(8453) ``` ### `quoteSwap` Query the provider's V3 quoter — no signing, no broadcast. Use this to render expected output, gas estimate, and price movement before the user commits. ```ts theme={null} import { parseUnits } from 'viem' const quote = await uni.quoteSwap({ type: 'exactInputSingle', tokenIn: 'USDC', tokenOut: 'WETH', fee: '0.05', amountIn: parseUnits('100', 6), // 100 USDC }) // quote.amountOut // quote.gasEstimate // quote.initializedTicksCrossed // quote.sqrtPriceX96After ``` `type` can be `"exactInputSingle"` or `"exactOutputSingle"`. Pass `amountIn` for exact input quotes. For exact output quotes, pass the desired output amount as `amountOutMinimum`: ```ts theme={null} const exactOutputQuote = await uni.quoteSwap({ type: 'exactOutputSingle', tokenIn: 'USDC', tokenOut: 'WETH', fee: '0.05', amountIn: 0n, // ignored for exactOutputSingle amountOutMinimum: parseUnits('0.03', 18), // desired WETH output }) ``` ### `getPool` Find the pool address for a pair and fee tier. Returns `null` when no pool exists — handy for switching fee tier or skipping the pair. ```ts theme={null} const pool = await uni.getPool({ token0: 'USDC', token1: 'WETH', fee: '0.05', }) if (!pool) { // Try a different fee tier or route } ``` ### `swapExactInputSingle` Build an approval + swap `Call[]` for an exact-input swap. Pass the calls through `to.calls` / `calls` to embed inside an intent, or send them directly from a wallet. ```ts theme={null} import { parseUnits } from 'viem' const calls = uni.swapExactInputSingle({ tokenIn: 'USDC', tokenOut: 'WETH', fee: '0.05', recipient: userEoa, amountIn: parseUnits('100', 6), // 100 USDC amountOutMinimum: parseUnits('0.03', 18), // 0.03 WETH floor }) ``` SushiSwap V3 uses the same fields plus a required `deadline`: ```ts theme={null} import { sushiswapV3 } from '0xtrails' import { parseUnits } from 'viem' const sushi = sushiswapV3.onChain('base') const deadline = BigInt(Math.floor(Date.now() / 1000) + 20 * 60) const calls = sushi.swapExactInputSingle({ tokenIn: 'USDC', tokenOut: 'WETH', fee: '0.05', recipient: userEoa, deadline, amountIn: parseUnits('100', 6), amountOutMinimum: parseUnits('0.03', 18), }) ``` ### `swapExactOutputSingle` Build an approval + swap `Call[]` for an exact-output swap. ```ts theme={null} import { parseUnits } from 'viem' const calls = uni.swapExactOutputSingle({ tokenIn: 'USDC', tokenOut: 'WETH', fee: '0.05', recipient: userEoa, amountOut: parseUnits('0.03', 18), // 0.03 WETH exactly amountInMaximum: parseUnits('100', 6), // 100 USDC ceiling }) ``` For SushiSwap V3 exact-output calls, include `deadline` as well. ### Address lookups Top-level helpers return contract addresses per chain: ```ts theme={null} uniswapV3.swapRouterAddressOn('base') uniswapV3.factoryAddressOn('base') sushiswapV3.swapRouterAddressOn('base') sushiswapV3.factoryAddressOn('base') ``` ### Capability checks Not every chain has both providers deployed. `isSupportedOn` returns `false` instead of throwing — useful for falling back between providers or warning the user. ```ts theme={null} import { uniswapV3, sushiswapV3 } from '0xtrails' const adapter = uniswapV3.isSupportedOn('base') ? uniswapV3.onChain('base') : sushiswapV3.isSupportedOn('base') ? sushiswapV3.onChain('base') : null if (!adapter) { // Neither provider is deployed on this chain — surface a friendly error. } ``` ## `lend` — supply into a money-market Use for supported lending markets such as Aave. **Use [`useEarnMarkets`](/sdk/composable-actions/markets-and-providers) to fetch the `marketId`.** ```ts theme={null} lend({ marketId: 'base-usdc-aave-v3-lending', amount: '50', }) ``` Optional: `receiverAddress` defaults to the connected user wallet, so aTokens / fTokens / cTokens land on the user's EOA automatically. Only override when you want the position to belong to a different owner. ## `deposit` — deposit into a vault Use for vault-shaped markets (ERC-4626, Morpho, Yearn, SummerFi, Sky, …). ```ts theme={null} deposit({ marketId: 'base-usdc-pusdc-0x1401d1271c47648ac70cbcdfa3776d4a87ce006b-4626-vault', amount: '100', }) ``` Optional params: * `receiverAddress` — **defaults to the connected user wallet** so vault shares land on the user's EOA. ## `assertCondition` — on-chain guard If the condition is false at execution time the entire batch reverts. ```ts theme={null} // Ensure the swap really produced >= 2400 USDC assertCondition({ erc20Balance: { token: 'USDC', gte: '2400' } }) // Native balance check assertCondition({ nativeBalance: { gte: '0.5' } }) // Deadline (unix timestamp) assertCondition({ notExpired: { deadline: 1712600000 } }) // Allowance check assertCondition({ erc20Allowance: { token: 'USDC', spender: '0x…', gte: '1000' }, }) ``` `assertCondition` is the only builder that does **not** accept `dynamic()` — its values must be known at quote time. ## `custom` — arbitrary contract call (escape hatch) Use when nothing above fits. Pair with `buildCall` and `erc20Utils` for ergonomic calldata — see [ERC-20 Helpers](/sdk/composable-actions/erc20-helpers). ```ts theme={null} import { custom, erc20Utils } from '0xtrails' import { encodeFunctionData, parseUnits } from 'viem' const MY_CONTRACT = '0x0000000000000000000000000000000000001234' const usdc = erc20Utils.USDC.addressOn('base') const actions = [ custom({ ...erc20Utils.approve({ tokenAddress: usdc, spender: MY_CONTRACT, amount: parseUnits('100', 6), }), }), custom({ to: MY_CONTRACT, data: encodeFunctionData({ abi: stakingAbi, functionName: 'stake', args: [parseUnits('100', 6)], }), }), ] ``` If custom calldata contains a `dynamic()` amount, pass `dynamicAmountToken` so the encoder knows which ERC-20 balance to hydrate at execution time. The only exception is a bare ERC-20 `approve(spender, dynamic())`, where the token is inferred from the call target. # Dynamic Values Source: https://docs.trails.build/sdk/composable-actions/dynamic-values Runtime placeholders for amounts and addresses: dynamic() and self() `dynamic()` and `self()` are runtime placeholders that resolve on the destination chain at execution time. They let you write action batches that adapt to whatever funds actually arrive, without predicting bridge fees or slippage. ```ts theme={null} import { dynamic, self } from '0xtrails' ``` ## `dynamic()` Means "use whatever balance the intent wallet has at execution time". Accepted on any amount field of any builder **except** [`assertCondition`](/sdk/composable-actions/building-actions#assertcondition-on-chain-guard). The killer use case is chaining. You don't know exactly how much USDC will arrive on the destination chain (bridge fees, slippage, price moves), but you know you want to deposit all of it: ```ts theme={null} import { lend, dynamic } from '0xtrails' // Whatever USDC arrives, lend it all. const actions = [ lend({ marketId: 'base-usdc-aave-v3-lending', amount: dynamic(), }), ] ``` ### Multi-step chaining Each consumer marks its input `dynamic()`: ```ts theme={null} import { swap, lend, dynamic } from '0xtrails' const actions = [ swap({ tokenIn: 'USDC', tokenOut: 'WETH', amountIn: dynamic(), // spend whatever USDC arrived minAmountOut: '0.03', }), lend({ marketId: 'base-weth-aave-v3-lending', amount: dynamic(), // lend whatever WETH the swap produced }), ] ``` ## `self()` Returns a placeholder address that resolves to the intent wallet at execution time, in case you need this address which is not known before executing. ```ts theme={null} import { swap, self } from '0xtrails' swap({ tokenIn: 'USDC', tokenOut: 'WETH', amountIn: '100', recipient: self(), // force WETH to stay on the intent wallet }) ``` `swap` picks a sensible default (`self()` for intermediate actions, the user's wallet for the final action), so you rarely need it — it's there for unusual compositions where you want to be explicit. # ERC-20 Helpers Source: https://docs.trails.build/sdk/composable-actions/erc20-helpers Token registry, calldata builders, and slippage helper for composing actions Side-effect-free helpers for building ERC-20 calls and looking up tokens. Pair them with [`custom`](/sdk/composable-actions/building-actions#custom-arbitrary-contract-call-escape-hatch) for ergonomic custom calldata. ```ts theme={null} import { parseUnits } from 'viem' import { erc20Utils, buildCall, buildApproveAndCall, buildErc20Approve, getAmountWithSlippage, } from '0xtrails' ``` ## Token registry — `erc20Utils` Look up canonical token metadata (address + decimals) per chain. ```ts theme={null} erc20Utils.USDC.onChain('base') // → { address: "0x833…", decimals: 6, name: "USD Coin", imageUrl: "…" } erc20Utils.USDC.addressOn('base') erc20Utils.USDC.decimalsOn('base') ``` ### Generic symbol lookup For symbols not pinned as top-level properties: ```ts theme={null} erc20Utils.token('WETH').onChain(42161) ``` ### ERC-20 calldata builders Standard calls, amounts are raw `bigint` wei: ```ts theme={null} const amount = parseUnits('1', 6) // 1 USDC erc20Utils.approve({ tokenAddress, spender, amount }) erc20Utils.transfer({ tokenAddress, to, amount }) erc20Utils.transferFrom({ tokenAddress, from, to, amount }) ``` Each returns a `Call`. To include one in an action list, spread it into `custom({ ...call })`: ```ts theme={null} import { custom, erc20Utils } from '0xtrails' import { parseUnits } from 'viem' custom({ ...erc20Utils.approve({ tokenAddress: erc20Utils.USDC.addressOn('base'), spender: '0xMyContract…', amount: parseUnits('100', 6), }), }) ``` ## Composing calls — `buildCall` / `buildApproveAndCall` `buildCall` constructs a `Call` from an ABI + function name + args: ```ts theme={null} import { buildCall } from '0xtrails' const stake = buildCall({ to: STAKING_CONTRACT, data: { abi: stakingAbi, functionName: 'stake', args: [parseUnits('100', 6)], }, }) ``` `buildApproveAndCall` bundles an approval **and** a follow-up call into a `[approve, call]` tuple: ```ts theme={null} import { buildApproveAndCall, erc20Utils } from '0xtrails' const [approve, stakeCall] = buildApproveAndCall({ token: erc20Utils.USDC.addressOn('base'), call: stake, // amount defaults to maxUint256 }) ``` ## Slippage helper — `getAmountWithSlippage` Subtracts `bps` basis points (1 bp = 0.01%) from a raw `bigint` amount. ```ts theme={null} getAmountWithSlippage(parseUnits('1', 6), 50) // 1 USDC, 0.5% → 0.995 USDC getAmountWithSlippage(parseUnits('1', 6), 500) // 1 USDC, 5% → 0.95 USDC ``` Handy for computing a safe `minAmountOut` or `amountOutMinimum` before handing it to [`swap`](/sdk/composable-actions/building-actions#swap-on-chain-token-swap) or the [chain-scoped Uniswap V3 / SushiSwap V3 adapters](/sdk/composable-actions/building-actions#swap-utilities). # Markets & Providers Source: https://docs.trails.build/sdk/composable-actions/markets-and-providers Discover markets and supported providers at runtime Every `lend` and `deposit` action needs a `marketId`. ## React hooks Use `useEarnMarkets`, `useEarnProviders`, and `useEarnBalances` inside a React tree wrapped with `TrailsProvider`: ```tsx theme={null} import { useEarnMarkets, useEarnProviders, useEarnBalances } from '0xtrails' function MarketPicker({ walletAddress }: { walletAddress: `0x${string}` }) { const { data: providers } = useEarnProviders() const { data: markets, total, isLoading } = useEarnMarkets({ chain: 'base', type: 'lending', // "lending" | "vault" provider: 'aave', // optional search: 'usdc', // optional free-text sortBy: 'rewardRateDesc', limit: 20, offset: 0, }) const { data: balances } = useEarnBalances({ walletAddress, chain: 'base', }) // markets[i].id → pass to lend / deposit // balances[i].yieldId → match against market.id } ``` ### `useEarnMarkets` filters | Field | Type | Description | | - | - | - | | `chain` | `ChainIdentifier` | Chain name or ID | | `type` | `"lending"` \| `"vault"` \| … | Market category | | `provider` | `ProviderId` | Restrict to one protocol | | `search` | `string` | Free-text search over market names | | `sortBy` | `SortBy` | e.g. `"rewardRateDesc"` | | `limit` | `number` | Page size | | `offset` | `number` | Page offset | Each `EarnMarket` exposes `id`, `rewardRate`, `statistics.tvlUsd`, `metadata.*`, `providerId`, and more. Use these hooks in browser apps to discover supported markets, providers, and the user's existing earn positions before building an action list. ### `useEarnBalances` Fetch a wallet's earn balances for one chain: ```tsx theme={null} const { data: balances, errors, isLoading } = useEarnBalances({ walletAddress: '0x9ec762D784B653E168d93eA3078B023d8958dBc6', chain: 'polygon', }) ``` Fetch balances across multiple chains in a single backend request: ```tsx theme={null} const { data: balances } = useEarnBalances({ walletAddress, chains: ['polygon', 'ethereum', 'base'], }) ``` Restrict the lookup to one market: ```tsx theme={null} const { data: balances } = useEarnBalances({ walletAddress, chain: 'polygon', marketId: 'polygon-usdc-aave-v3-lending', }) ``` | Field | Type | Description | | - | - | - | | `walletAddress` | `string` | Wallet whose earn positions should be fetched | | `chain` | `ChainIdentifier` | Single chain name, ID, or viem chain object | | `chains` | `ChainIdentifier[]` | Multiple chains; mutually exclusive with `chain` | | `marketId` | `string` | Optional market ID to filter one position | | `arguments` | `object` | Optional market-specific balance arguments | | `enabled` | `boolean` | Set `false` to disable the query | ## End-to-end example Wire a market picker into a typed action: ```tsx theme={null} import { useQuote, useEarnMarkets, lend, } from '0xtrails' function LendWithMarketPicker() { const { data: markets } = useEarnMarkets({ chain: 'base', type: 'lending', search: 'USDC', sortBy: 'rewardRateDesc', limit: 5, }) const market = markets... // Choose your market const { send } = useQuote({ from: { chain: 'arbitrum', token: 'USDC' }, to: { chain: 'base', token: 'USDC', amount: '100' }, actions: market ? [lend({ marketId: market.id, amount: "100" })] : [], }) return ( ) } ``` # Overview Source: https://docs.trails.build/sdk/composable-actions/overview Composable Actions allow developers to easily specify multiple actions to take on a destination chain that can compose with multiple protocols. 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. Two browser integration styles are supported: 1. **Quote-first flow** — use [`useQuote`](/sdk/hooks#usequote-hook) with `actions` when you want to build your own UI and preview a quote before sending it. 2. **Send-transaction flow** — use `useTrailsSendTransaction` with `actions` when you want to trigger the Trails Widget. Token amounts passed to `useQuote` and `useTrailsSendTransaction` are human-readable decimal strings, so `"0.2"` means 0.2 USDC / USDT / ETH depending on the selected token. ## What Composable Actions are A Trails **intent** is a two-phase transaction: ```mermaid theme={null} flowchart LR User["User's funds
(origin chain)"] Wallet["Intent wallet
(destination chain)"] Calls["Destination calls"] User -->|bridge / swap| Wallet Wallet --> Calls ``` 1. The user signs **once** on the origin chain. Their funds are bridged and/or swapped to an **intent wallet** on the destination chain. 2. Trails then runs the **destination calls** from that intent wallet. Composable Actions are a typed builder for those destination calls. Each builder returns a plain action descriptor, an array of them is what you hand to `useQuote` or `useTrailsSendTransaction`. ## Available action builders Import the builders you need directly from `0xtrails`: ```ts theme={null} import { swap, lend, deposit, assertCondition, custom } from '0xtrails' ``` | Builder | What it does | | - | - | | `swap({...})` | On-chain swap via Uniswap V3 or SushiSwap V3 | | `lend({...})` | Supply into supported lending markets such as Aave | | `deposit({...})` | Deposit into an ERC-4626 / vault-style market (Morpho, Yearn, SummerFi, Sky, …) | | `assertCondition({...})` | On-chain guard (`balance >= X`, `deadline > now`, …) — whole batch reverts if false | | `custom({...})` | Wrap an arbitrary contract call (escape hatch) | See [Building Actions](/sdk/composable-actions/building-actions) for the full API of each builder. ## Demo Try composable actions in the [SDK sandbox](https://demo.trails.build/sdk-sandbox/composable-actions). ## Where to go next * [Quickstart](/sdk/composable-actions/quickstart) — a multi-step example combining `deposit`, `swap`, `assertCondition`, and `lend` in a single intent. * [Building Actions](/sdk/composable-actions/building-actions) — the full builder API, one section per builder. * [Dynamic Values](/sdk/composable-actions/dynamic-values) — `dynamic()` and `self()` for runtime amount / address resolution. * [Markets & Providers](/sdk/composable-actions/markets-and-providers) — discover markets and providers. * [ERC-20 Helpers](/sdk/composable-actions/erc20-helpers) — token registry, calldata builders, slippage helper. * [Supported Chains](/sdk/composable-actions/supported-chains) — chain identifiers and swap provider coverage. # Quickstart Source: https://docs.trails.build/sdk/composable-actions/quickstart Compose multiple DeFi steps into a single cross-chain intent The whole point of Composable Actions is composition — more than one call on the destination chain. This example splits a single intent into four destination steps: 1. **Deposit** 0.1 USDT into a Morpho vault. 2. **Swap** the leftover USDT to USDC. 3. **Assert** the intent wallet has at least 0.08 USDC before continuing. 4. **Lend** the resulting USDC into an Aave USDC market. The user pays with USDC on Arbitrum. Trails bridges/swaps to deliver 0.2 USDT on Polygon, then runs the four steps from the intent wallet. The `marketId`s below are hard-coded for clarity. In a real app, use [`useEarnMarkets`](/sdk/composable-actions/markets-and-providers#react-hooks) to discover what's available for a given chain / token / provider and grab the `id` from the result — no need to keep a list of IDs in your code. ## Use with `useTrailsSendTransaction` Use `useTrailsSendTransaction` when you want a button-driven flow with the Trails modal. Put the `actions` array directly in the hook config, then call `sendTransaction` with the destination token requirement. ```tsx theme={null} import { useTrailsSendTransaction, dynamic, deposit, swap, lend, assertCondition, erc20Utils, } from '0xtrails' const morphoMarketId = 'polygon-usdt-bbqusdt0-0xb7c9988d3922f25a336a469f3bb26ca61fe79e24-4626-vault' const aaveMarketId = 'polygon-usdc-aave-v3-lending' export function ComposedEarnSendTransactionButton({ recipient, }: { recipient: `0x${string}` }) { const { sendTransaction, isPending, error } = useTrailsSendTransaction({ actions: [ // a) Deposit 0.1 USDT -> Morpho vault. deposit({ marketId: morphoMarketId, amount: '0.1', }), // b) Swap whatever USDT is left -> USDC. swap({ tokenIn: 'USDT', tokenOut: 'USDC', amountIn: dynamic(), fee: '0.3', // pool tier 0.3% }), // c) Guard: make sure the swap really gave us at least 0.08 USDC. assertCondition({ erc20Balance: { token: 'USDC', gte: '0.08' }, }), // d) Lend all the USDC we just received into Aave. lend({ marketId: aaveMarketId, amount: dynamic(), }), ], onStatusUpdate: (transactionStates) => { for (const state of transactionStates) { console.log('Transaction:', state) } }, }) return ( ) } ``` When origin fields are omitted, Trails opens the modal so the user can choose what token and chain to pay from. ## Use with `useQuote` Use `useQuote` when you want to show quote state before sending and you don't want to use the widget. The same `actions` array goes directly into the hook config. ```tsx theme={null} import { useQuote, dynamic, deposit, swap, lend, assertCondition, } from '0xtrails' const morphoMarketId = 'polygon-usdt-bbqusdt0-0xb7c9988d3922f25a336a469f3bb26ca61fe79e24-4626-vault' const aaveMarketId = 'polygon-usdc-aave-v3-lending' export function ComposedEarnQuoteButton() { 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: (transactionStates) => { for (const state of transactionStates) { console.log('Transaction:', state) } }, }) if (isLoadingQuote) return

Quoting…

if (quoteError) return

Error: {quoteError.message}

return ( ) } ``` ## What's happening here * **Concrete amount** on the first action (`"0.1"`) splits off a fixed slice. **`dynamic()`** on subsequent actions consumes whatever the previous step produced — no need to predict slippage or bridge fees. See [Dynamic Values](/sdk/composable-actions/dynamic-values) for details. * Actions run **top-down** in the same destination batch. If any step reverts — including [`assertCondition(...)`](/sdk/composable-actions/building-actions#assertcondition-on-chain-guard) guards — the whole destination batch reverts atomically, so partial state is never left behind. * `tokenAmount` in `useTrailsSendTransaction` and `to.amount` in `useQuote` are human-readable decimal strings. The split into `0.1` + the rest happens inside the intent wallet on the destination chain. * `onStatusUpdate` fires as each hop lands — origin approval, bridge, destination batch — so you can wire it into a progress UI. ## Next steps * Explore different actions in [Building Actions](/sdk/composable-actions/building-actions). * Learn how `dynamic()` and `self()` work under the hood in [Dynamic Values](/sdk/composable-actions/dynamic-values). * Discover markets and providers with [Markets & Providers](/sdk/composable-actions/markets-and-providers). # Supported Chains Source: https://docs.trails.build/sdk/composable-actions/supported-chains Chain identifiers and swap provider coverage for composable actions ## Chain identifiers Chain identifiers are either **names** or **chain IDs**. The following names are accepted wherever a `ChainIdentifier` is expected (`useQuote`, the action builders, `useEarnMarkets`, `uniswapV3.onChain(...)`, …): ``` apechain, arbitrum, arbitrum-nova, avalanche, avalanche-c, b3, base, berachain, binance, bsc, bnb, etherlink, gnosis, hyperevm, katana, mainnet, ethereum, monad, optimism, polygon, soneium, somnia, sonic ``` Chain IDs (e.g. `8453` for Base, `42161` for Arbitrum) work interchangeably. # Configuration Source: https://docs.trails.build/sdk/configuration Complete configuration reference for the Trails SDK ## Focused components ### Shared props All focused components accept these optional props in addition to their mode-specific props. #### Appearance | Prop | Type | Default | Description | | - | - | - | - | | `theme` | `"light" \| "dark" \| "auto"` | `"auto"` | Color theme; `auto` follows system preference | | `customCss` | `string \| Record` | — | Custom CSS to inject into the widget | | `disableCss` | `boolean` | `false` | Disable default CSS injection | | `renderInline` | `boolean` | `false` | Render inline instead of as a modal | | `buttonText` | `string` | — | Custom CTA button text | #### App metadata Used in wallet connection dialogs (WalletConnect, etc.): ```tsx theme={null} appMetadata={{ name: "My App", url: "https://myapp.com", imageUrl: "https://myapp.com/logo.png", description: "Short description", }} ``` #### Wallet options | Prop | Type | Description | | - | - | - | | `walletConnectProjectId` | `string` | WalletConnect project ID | | `walletOptions` | `string[]` | Wallet connector IDs to show (e.g. `["metamask", "walletconnect"]`) | | `adapters` | `TrailsAdapterEntry[]` | Explicit wallet runtime adapters. Use `wagmiAdapter` from [`@0xtrails/adapter-wagmi`](/sdk/adapters/wagmi) to share an existing wagmi session, or build your own with [`evmAdapter`](/sdk/adapters/eip-1193). See [Adapters](/sdk/adapters/overview). | | `hideAddWallet` | `boolean` | Hide "Add wallet" option | | `isSmartWallet` | `boolean` | Signal that the connected wallet is a smart wallet | The `wagmiConnectors` and `decoupleWagmi` props were removed in [`0xtrails@0.16.0`](/sdk/changelog/0xtrails-0.16.0). Wagmi is no longer required by default — pass adapters on the widget's `adapters` prop instead. See [Adapters](/sdk/adapters/overview). #### Route options | Prop | Type | Description | | - | - | - | | `slippageTolerance` | `number \| string` | Slippage tolerance (e.g. `0.005` for 0.5%) | | `swapProvider` | `RouteProvider` | Preferred same-chain swap provider | | `swapProviderFallback` | `boolean` | Fall back if preferred swap provider is unavailable | | `bridgeProvider` | `RouteProvider` | Preferred bridge provider | | `bridgeProviderFallback` | `boolean` | Fall back if preferred bridge provider is unavailable | | `priceImpactWarningThresholdBps` | `number` | Basis points before showing a price impact warning | | `priceImpactWarningMessage` | `string` | Custom price impact warning message | | `priceImpactFallbackBridgeUrl` | `string` | URL to redirect when price impact is too high | | `intentProtocolVersion` | `IntentProtocolVersion` | Force a specific intent protocol version | #### Debug / dev ```tsx theme={null} dev={{ debug: true, sequenceIndexerUrl: "https://...", trailsApiUrl: "https://...", }} ``` #### General callbacks These callbacks are available on all focused components (in addition to mode-specific lifecycle callbacks): | Callback | When it fires | | - | - | | `onOpen` | Widget modal opens | | `onClose` | Widget modal closes | | `onOriginConfirmation` | Origin-chain transaction confirmed | | `onDestinationConfirmation` | Destination-chain transaction confirmed | | `onQuote` | Quote is received | | `onSignRequest` | Wallet signature requested | | `onSign` | Wallet signature obtained | | `onSignReject` | User rejected the signature | | `onApproveRequest` | ERC-20 approval requested | | `onApprove` | ERC-20 approval confirmed | | `onApproveReject` | User rejected the approval | | `onStatus` | Transaction status update | ### ChainIdentifier All `chain` fields accept a `ChainIdentifier`, which can be: * A chain name string: `"base"`, `"ethereum"`, `"polygon"`, `"arbitrum"`, `"optimism"`, `"avalanche"`, `"bsc"`, `"katana"`, etc. * A numeric chain ID: `8453`, `1`, `137`, etc. * A viem `Chain` object ### PaymentMethod The `paymentMethod` prop on focused components accepts: | Value | Method | | - | - | | `"CONNECTED_WALLET"` | Connected wallet (default when omitted) | | `"CRYPTO_TRANSFER"` | QR code / address deposit | | `"CREDIT_DEBIT_CARD"` | Fiat on-ramp (credit/debit card) | | `"EXCHANGE"` | CEX transfer (Coinbase, Binance, etc.) | ### Mode-specific props See the individual mode pages for full prop details: * [Pay](/sdk/modes/pay) — `to.recipient`, `to.currency`, `to.chain`, `to.amount`, `payMessage`, `onPaymentStart/Success/Error` * [Fund](/sdk/modes/fund) — `to`, `fundMethodsList`, `hideUnlistedFundMethods`, `hideSwap`, `onFundingStart/Success/Error` * [Swap](/sdk/modes/swap) — `from`, `to`, `onSwapStart/Success/Error` * [Withdraw](/sdk/modes/withdraw) — `from`, `to`, `onWithdrawStart/Success/Error` * [Earn](/sdk/modes/earn) — `to` with `calldata`, `onEarnStart/Success/Error` # Customization Source: https://docs.trails.build/sdk/customization Customize the Trails widget appearance with CSS variables To customise the widget, pass CSS variables through the `customCss` prop. You can pass a string of CSS declarations or a key-value object — both formats work. ```tsx theme={null} import { Pay } from '0xtrails/widget' // Object form also accepted ``` ## Match your brand colours Override the primary colour and its interactive states: ```tsx theme={null} ``` To pull in colours from your existing design system, reference your own CSS variables directly: ```tsx theme={null} customCss={{ '--trails-primary': 'var(--color-brand-500)', '--trails-primary-hover': 'var(--color-brand-600)', '--trails-bg-primary': 'var(--color-surface)', '--trails-text-primary': 'var(--color-text-default)', }} ``` ## Set the border radius style Rounded theme: ```tsx theme={null} customCss={` --trails-border-radius-widget: 32px; --trails-border-radius-button: 24px; --trails-border-radius-input: 20px; --trails-border-radius-dropdown: 16px; `} ``` Square / no-radius theme: ```tsx theme={null} customCss={` --trails-border-radius-widget: 0px; --trails-border-radius-button: 0px; --trails-border-radius-input: 0px; --trails-border-radius-dropdown: 0px; --trails-border-radius-container: 0px; `} ``` ## Set the font family ```tsx theme={null} customCss={` --trails-font-family: "Inter", "SF Pro Display", -apple-system, sans-serif; `} ``` Monospace: ```tsx theme={null} customCss={` --trails-font-family: "Fira Code", "Monaco", monospace; `} ``` ## Configure dark mode Use the `theme` prop for light/dark/auto. If you need to override colours for a specific theme, combine `theme` with `customCss`: ```tsx theme={null} ``` ## Advanced: glassmorphism ```tsx theme={null} customCss={` --trails-bg-primary: rgba(255, 255, 255, 0.1); --trails-bg-secondary: rgba(255, 255, 255, 0.05); --trails-border-primary: rgba(255, 255, 255, 0.2); --trails-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); `} ``` ## Advanced: disable default CSS To apply a fully custom stylesheet without Trails' defaults interfering: ```tsx theme={null} ``` ## Global CSS overrides Variables cascade as normal CSS custom properties. You can set them globally or scope them to a container: ```css theme={null} /* Apply to all widget instances */ :root { --trails-primary: #6366f1; } /* Scope to a specific container */ .my-payment-section { --trails-primary: #10b981; } ``` *** ## Widget copy overrides Pass `labels` to replace the widget's default copy. Every key is optional, and a blank string falls back to the built-in text. ```tsx theme={null} import { Fund } from '0xtrails/widget' ``` | Key | What it renames | Default | | - | - | - | | `confirmButton` | The action verb on the primary form button, and on the modal trigger button | `Fund`, `Pay`, `Swap`, `Deposit`, `Withdraw` by mode; the modal trigger shows `Earn` in earn mode | | `formTitle` | The amount form screen, and the crypto-transfer screens that continue it, so the header stays put across the flow | `Fund`, `Pay`, `Swap`, `Withdraw`, `Earn` by mode; `Deposit` on the transfer screens | | `methodsTitle` | The payment method picker | `Choose a funding method`; `Choose a payment method` in pay and earn modes; `Payment methods` on the pay flow's own picker | | `tokensTitle` | The screen where the user picks what to pay *with* | Varies by mode — `Fund with any token`, `Select From Token`, and others | Overrides apply only to the **configured mode's** screens. With `mode="fund"`, a `confirmButton` override does not follow the user onto the swap sub-screen reached from the fund header. Destination token pickers keep their own titles, and transient button states — "Enter an amount", "Getting quote...", "Connect your wallet", "Processing...", "Insufficient Balance" — are never overridden. Where a modal trigger button is rendered, an explicit `buttonText` prop takes precedence over `labels.confirmButton`. *** ## Consent checkbox Pass `consentCheckbox` to render a consent line directly above the primary form button. ```tsx theme={null} Terms & Conditions`, requiredMessage: 'Please accept the Terms & Conditions to continue.', }} /> ``` | Key | Required | Purpose | | - | - | - | | `label` | Yes | The consent line, as inline HTML | | `requiredMessage` | No | Standing notice shown under the checkbox after the button is pressed unticked | Setting the prop makes the tick **mandatory** — there is no separate required flag. While the box is unticked the button is not disabled by the checkbox; pressing it flags the row instead of submitting, so a ready quote still reads as ready. A failed press moves focus to the checkbox. With `requiredMessage` set, the notice stands under the row until the box is ticked and is announced to assistive technology. Without it, a failed press shakes and tints the row, and the tint clears itself (both are skipped under `prefers-reduced-motion`). The row is not a `