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
publicClientandwalletClient - 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
expiryhas passed. - Rebuild if the wallet delays execution or changes account.
- Recheck allowance when the selected route changes.
- Show
minAmountOut, not only the optimisticamountOut. - 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
})