RathRath Finance
Use Cases

Same-Chain Token Swap

Quote, approve, and execute a token swap on one chain with xPath.

This guide swaps an ERC-20 input token for another token on the same EVM chain. In a same-chain route, fromChain and toChain are identical and the returned route kind should be sameChainSwap.

Prerequisites

  • xPath API key
  • Source-chain publicClient and walletClient
  • Input token balance and native gas
  • Input and output token addresses from xPath

Request Same-Chain Routes

const API_URL = 'https://api.xpath.rath.fi'
const API_KEY = process.env.XPATH_API_KEY!
const account = walletClient.account.address

async function xpath(path: string, init?: RequestInit) {
  const response = await fetch(`${API_URL}${path}`, {
    ...init,
    headers: {
      'api-key': API_KEY,
      'Content-Type': 'application/json',
      ...init?.headers,
    },
  })
  if (!response.ok) throw new Error(await response.text())
  const body = await response.json()
  if (body.code !== 0) throw new Error(body.message)
  return body.data
}

const params = new URLSearchParams({
  fromChain: '8453',
  toChain: '8453',
  fromToken: '0x4200000000000000000000000000000000000006',
  toToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  amount: '1000000000000000',
  sender: account,
  receiver: account,
  slippage: '1',
  routeMode: 'max_value',
})

const routes = await xpath(`/quote?${params}`)
const route = routes.find((candidate) => candidate.routeKind === 'sameChainSwap')

if (!route) throw new Error('No same-chain route is currently available')

Validate the Selected Route

Before execution, inspect the output and protection fields:

console.table({
  input: route.amount,
  expectedOutput: route.amountOut,
  minimumOutput: route.minAmountOut,
  routeFeeUsd: route.fees.routeFeeUsd,
  estimatedTimeMs: route.estimatedTimeMs,
  expiresAt: route.expiry,
})

if (route.expiry && Date.now() >= route.expiry * 1000) {
  throw new Error('Quote expired; request a new quote')
}

Build the Transaction

const tx = await xpath('/build-path-by-id', {
  method: 'POST',
  body: JSON.stringify({
    quoteId: route.quoteId,
    simulation: true,
  }),
})

if (tx.chain !== 8453) throw new Error(`Switch wallet to chain ${tx.chain}`)

Approve the Allowance Target

import { erc20Abi } from 'viem'

const spender = tx.allowanceTarget ?? tx.to

const allowance = await publicClient.readContract({
  address: route.fromToken.address,
  abi: erc20Abi,
  functionName: 'allowance',
  args: [account, spender],
})

if (allowance < BigInt(route.amount)) {
  const approvalHash = await walletClient.writeContract({
    address: route.fromToken.address,
    abi: erc20Abi,
    functionName: 'approve',
    args: [spender, BigInt(route.amount)],
  })
  await publicClient.waitForTransactionReceipt({ hash: approvalHash })

  // Request and build a fresh route before execution because quotes are short-lived.
}

Approve allowanceTarget when returned. For xPath-wrapped EVM routes, the transaction target can be the spender when allowanceTarget is null.

Submit and Confirm

const hash = await walletClient.sendTransaction({
  to: tx.to,
  data: tx.data,
  value: BigInt(tx.value ?? '0'),
  gas: BigInt(tx.gasLimit),
})

const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('Swap confirmed in block:', receipt.blockNumber)

Failure Handling

  • Request a new quote if expiry has passed.
  • Rebuild if the wallet delays execution or changes account.
  • Recheck allowance when the selected route changes.
  • Show minAmountOut, not only the optimistic amountOut.
  • Treat a reverted source transaction as a failed swap; it was not submitted to a bridge.

Full Script

npm install viem

export XPATH_API_KEY="YOUR_API_KEY"
export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
export RPC_URL="https://mainnet.base.org"
import {
  createPublicClient,
  createWalletClient,
  erc20Abi,
  http,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { base } from 'viem/chains'

const API_URL = 'https://api.xpath.rath.fi'
const API_KEY = process.env.XPATH_API_KEY!
const account = privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`)

const publicClient = createPublicClient({
  chain: base,
  transport: http(process.env.RPC_URL),
})

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(process.env.RPC_URL),
})

async function xpath(path: string, init?: RequestInit) {
  const response = await fetch(`${API_URL}${path}`, {
    ...init,
    headers: {
      'api-key': API_KEY,
      'Content-Type': 'application/json',
      ...init?.headers,
    },
  })

  if (!response.ok) {
    throw new Error(`xPath request failed: ${response.status} ${await response.text()}`)
  }

  const body = await response.json()
  if (body.code !== 0) throw new Error(body.message)
  return body.data
}

async function main() {
  const sender = account.address
  const quoteParams = new URLSearchParams({
    fromChain: String(base.id),
    toChain: String(base.id),
    fromToken: '0x4200000000000000000000000000000000000006',
    toToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    amount: '1000000000000000',
    sender,
    receiver: sender,
    slippage: '1',
    routeMode: 'max_value',
  })

  async function getExecution() {
    const routes = await xpath(`/quote?${quoteParams}`)
    const route = routes.find(
      (candidate) => candidate.routeKind === 'sameChainSwap'
    )

    if (!route) throw new Error('No same-chain route is currently available')

    const tx = await xpath('/build-path-by-id', {
      method: 'POST',
      body: JSON.stringify({ quoteId: route.quoteId, simulation: true }),
    })

    return { route, tx }
  }

  let { route, tx } = await getExecution()

  console.log('Provider:', route.providers.map((provider) => provider.name))
  console.log('Expected output:', route.amountOut)
  console.log('Minimum output:', route.minAmountOut)

  if (tx.chain !== base.id) throw new Error(`Switch wallet to chain ${tx.chain}`)

  const spender = (tx.allowanceTarget ?? tx.to) as `0x${string}`
  const token = route.fromToken.address as `0x${string}`
  const amount = BigInt(route.amount)

  const allowance = await publicClient.readContract({
    address: token,
    abi: erc20Abi,
    functionName: 'allowance',
    args: [sender, spender],
  })

  if (allowance < amount) {
    const approvalHash = await walletClient.writeContract({
      address: token,
      abi: erc20Abi,
      functionName: 'approve',
      args: [spender, amount],
    })
    await publicClient.waitForTransactionReceipt({ hash: approvalHash })

    // Stored quotes expire quickly; rebuild against a fresh quote after approval.
    const fresh = await getExecution()
    route = fresh.route
    tx = fresh.tx

    const freshSpender = (tx.allowanceTarget ?? tx.to) as `0x${string}`
    const freshAllowance = await publicClient.readContract({
      address: route.fromToken.address as `0x${string}`,
      abi: erc20Abi,
      functionName: 'allowance',
      args: [sender, freshSpender],
    })

    if (freshAllowance < BigInt(route.amount)) {
      throw new Error('The fresh route uses a different spender; approve it and rebuild again')
    }
  }

  const hash = await walletClient.sendTransaction({
    to: tx.to as `0x${string}`,
    data: tx.data as `0x${string}`,
    value: BigInt(tx.value ?? '0'),
    gas: BigInt(tx.gasLimit),
  })

  const receipt = await publicClient.waitForTransactionReceipt({ hash })
  console.log('Swap transaction:', hash)
  console.log('Confirmed in block:', receipt.blockNumber)
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

On this page