Skip to main content

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.
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 Your API Key: Join the Trails Telegram group or email to request your API access key.

Core Workflow

Every interaction through Trails follows this flow:
1

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:
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);
2

Get a Quote

Request a quote to see rates, fees, and routing options for your transaction.
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.
3

Commit the Intent

Lock in the quote by committing the intent with the fetched contract addresses. This reserves the rates.
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);
You cannot change the contents of the intent, or the server will reject the commit. The intent object contains everything needed to relay and execute the intent.
Quotes expire 5 minutes after being issued. Once committed, you have 10 minutes to call ExecuteIntent. If either window expires, you’ll need to request a new quote.
4

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:
import { createWalletClient, createPublicClient, http, encodeFunctionData, erc20Abi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base, arbitrum, mainnet } from 'viem/chains';

// Create clients (or use wagmi hooks: useWalletClient, usePublicClient)
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()
});
// 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);
5

Monitor Completion

Wait for the transaction to complete using the streaming endpoint:
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:
{
  "webrpcError": {
    "code": 2000,
    "name": "InvalidArgument",
    "message": "Invalid token address",
    "cause": "originTokenAddress must be a valid EVM address",
    "httpStatus": 400
  }
}

Error Codes

CodeNameDescriptionHTTP Status
2000InvalidArgumentRequest parameter is invalid or malformed400
2001UnexpectedUnexpected server error500
2002UnavailableRequested resource is unavailable400
2003QueryFailedDatabase or external query failed400
2004IntentStatusIntent is in an invalid state for this operation422
7000IntentsSkippedTransaction doesn’t require an intent (same chain/token)400
7001QuoteExpiredQuote has expired, request a new one400
8000NotFoundResource not found400
8008UnsupportedNetworkChain ID is not supported422
8009ClientOutdatedSDK/client version is outdated422
9000IntentsDisabledIntents service is temporarily unavailable400

Handling Errors

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:
StageTime LimitWhat Happens
Quote validity5 minutesQuote expires, request a new one
After CommitIntent10 minutesMust call ExecuteIntent within this window
Intent executionN/ATrails handles execution timing
Always check quote expiration before committing. The intent.expiresAt timestamp indicates when the quote becomes invalid.

Rate Limiting

The API uses rate limiting to ensure service reliability. If you exceed limits, you’ll receive HTTP 429 responses.
Limit TypeValueNotes
Requests per second50Per API key
Burst limit100Short burst allowance
QuoteIntent calls20/minRate-limited separately
Best practices:
  • Cache GetChains, GetTokenList responses (they change infrequently)
  • Batch balance checks where possible
  • Implement exponential backoff on 429 responses
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 - Retrieve full intent details including quote, fees, and status
  • GetIntentReceipt - Poll for transaction status and get receipt with transaction hashes
  • WaitIntentReceipt - Stream intent updates with automatic polling until completion
  • GetIntentHistory - Get complete transaction history with receipts for a user’s wallet address

Support

Need help? Join our community: