Use Cases
Cross-Chain Swap
Quote, execute, and track a cross-chain swap between two xPath-supported chains.
This guide swaps an ERC-20 token from Base to Arbitrum. xPath can return a directBridge route when the same asset is available on both chains or a swapBridge route when a source or destination swap is required. Execution has two phases: submit the source-chain transaction, then track destination execution.
Prerequisites
- xPath API key
- Wallet connected to the source chain
- Input token balance and source-chain gas
- Recipient address valid on the destination chain
Request Cross-Chain Swap Routes
const API_URL = 'https://api.xpath.rath.fi'
const API_KEY = process.env.XPATH_API_KEY!
const sender = walletClient.account.address
const receiver = sender
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: '42161',
fromToken: '0x4200000000000000000000000000000000000006',
toToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
amount: '1000000000000000',
sender,
receiver,
slippage: '1',
routeMode: 'suggested',
})
const routes = await xpath(`/quote?${params}`)
const route = routes.find((candidate) =>
['directBridge', 'swapBridge'].includes(candidate.routeKind)
)
if (!route) throw new Error('No cross-chain swap route is currently available')Review Route Risk and Cost
console.log({
routeKind: route.routeKind,
providers: route.providers.map((provider) => provider.name),
expectedOutput: route.amountOut,
minimumOutput: route.minAmountOut,
estimatedTimeMs: route.estimatedTimeMs,
routeFeeUsd: route.fees.routeFeeUsd,
gasFeeUsd: route.gasFee.gasFeeUsd,
})Cross-chain completion time is an estimate. Do not promise completion based only on estimatedTimeMs.
Build and Approve
import { erc20Abi } from 'viem'
const tx = await xpath('/build-path-by-id', {
method: 'POST',
body: JSON.stringify({
quoteId: route.quoteId,
simulation: true,
}),
})
const spender = tx.allowanceTarget ?? tx.to
const allowance = await publicClient.readContract({
address: route.fromToken.address,
abi: erc20Abi,
functionName: 'allowance',
args: [sender, 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 })
// Quotes are short-lived. Request and build a fresh route after approval.
}Submit the Source Transaction
const sourceTxHash = await walletClient.sendTransaction({
to: tx.to,
data: tx.data,
value: BigInt(tx.value ?? '0'),
gas: BigInt(tx.gasLimit),
})
await publicClient.waitForTransactionReceipt({ hash: sourceTxHash })
console.log('Source transaction confirmed:', sourceTxHash)Source confirmation does not mean the destination tokens have arrived. Continue tracking the route.
Poll Cross-Chain Status
const terminalStatuses = new Set(['completed', 'failed'])
const timeoutAt = Date.now() + 30 * 60 * 1000
let routeStatus
while (Date.now() < timeoutAt) {
const params = new URLSearchParams({
txHash: sourceTxHash,
fromChain: String(tx.chain),
userAddress: sender,
})
routeStatus = await xpath(`/status?${params}`)
console.log('Route status:', routeStatus.status)
if (terminalStatuses.has(routeStatus.status.toLowerCase())) break
await new Promise((resolve) => setTimeout(resolve, 5000))
}
if (!routeStatus || !terminalStatuses.has(routeStatus.status.toLowerCase())) {
throw new Error('Cross-chain status polling timed out')
}
if (routeStatus.status.toLowerCase() === 'failed') {
throw new Error('Cross-chain execution failed')
}
console.log('Destination transaction:', routeStatus?.destTxDetails?.txHash)Production Recommendations
- Persist the source transaction hash, source chain, receiver, quote ID, and providers.
- Resume polling after page reload instead of relying on one browser session.
- Show source and destination explorer links independently.
- Distinguish source failure, cross-chain delay, and destination failure in the UI.
- Escalate long-running routes with the identifiers required by provider support.
Full Script
Install viem and provide the required environment variables:
npm install viem
export XPATH_API_KEY="YOUR_API_KEY"
export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
export SOURCE_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.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: '8453',
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 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('Route:', route.routeKind)
console.log('Providers:', 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.
;({ route, tx } = await getExecution())
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}`)
console.log('Cross-chain status:', status.status)
if (terminalStatuses.has(status.status.toLowerCase())) {
console.log('Destination transaction:', status.destTxDetails?.txHash)
if (status.status.toLowerCase() === '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
})