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.quoteIdwithPOST /build-path-by-id. - Approve
tx.allowanceTarget ?? tx.tofor ERC-20 routes. - Send the returned
to,data,value, andgasLimitfields unchanged. - Call
GET /statuswith bothtxHashandfromChain. - Persist the source hash so status tracking can resume after a reload.