RathRath Finance

End-to-End Transaction Example

Follow a complete xPath flow from quote to cross-chain execution and status tracking.

This example requests a Base-to-Arbitrum route, builds the selected quote, handles ERC-20 approval, submits the source transaction, and tracks the cross-chain result.

Setup

npm install viem

export XPATH_API_KEY="YOUR_API_KEY"
export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
export SOURCE_RPC_URL="https://mainnet.base.org"

Full Example

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.SOURCE_RPC_URL),
})

const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(process.env.SOURCE_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: '42161',
    fromToken: '0x4200000000000000000000000000000000000006',
    toToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
    amount: '1000000000000000',
    sender,
    receiver: sender,
    slippage: '1',
    routeMode: 'suggested',
  })

  async function getExecution() {
    const routes = await xpath(`/quote?${quoteParams}`)
    const route = routes.find((candidate) =>
      ['directBridge', 'swapBridge'].includes(candidate.routeKind)
    )

    if (!route) throw new Error('No cross-chain swap route is 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()
  if (tx.chain !== base.id) throw new Error(`Switch wallet to chain ${tx.chain}`)

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

  const token = route.fromToken.address as `0x${string}`
  const spender = (tx.allowanceTarget ?? tx.to) 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 })

    // xPath quote IDs expire quickly, so rebuild after the approval confirms.
    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 sourceTxHash = await walletClient.sendTransaction({
    to: tx.to as `0x${string}`,
    data: tx.data as `0x${string}`,
    value: BigInt(tx.value ?? '0'),
    gas: BigInt(tx.gasLimit),
  })

  await publicClient.waitForTransactionReceipt({ hash: sourceTxHash })
  console.log('Source transaction:', sourceTxHash)

  const terminalStatuses = new Set(['completed', 'failed'])
  const timeoutAt = Date.now() + 30 * 60 * 1000

  while (Date.now() < timeoutAt) {
    const statusParams = new URLSearchParams({
      txHash: sourceTxHash,
      fromChain: String(tx.chain),
      userAddress: sender,
    })
    const status = await xpath(`/status?${statusParams}`)
    const normalizedStatus = status.status.toLowerCase()

    console.log('Cross-chain status:', status.status)
    if (terminalStatuses.has(normalizedStatus)) {
      console.log('Destination transaction:', status.destTxDetails?.txHash)
      if (normalizedStatus === 'failed') process.exitCode = 1
      return
    }

    await new Promise((resolve) => setTimeout(resolve, 5000))
  }

  throw new Error('Cross-chain status polling timed out')
}

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

Keep the API key in a backend or protected proxy for browser applications. Never embed a production API key or private key in frontend code.

Important Fields

  • Use route.quoteId with POST /build-path-by-id.
  • Approve tx.allowanceTarget ?? tx.to for ERC-20 routes.
  • Send the returned to, data, value, and gasLimit fields unchanged.
  • Call GET /status with both txHash and fromChain.
  • Persist the source hash so status tracking can resume after a reload.

On this page