RathRath Finance
Use Cases

Gasless Swaps with Permit2

Execute same-chain and cross-chain swaps through Permit2 and Tachyon.

Gasless xPath routes use Permit2 typed-data authorization and Tachyon relay execution. The user signs the route authorization, while the relay submits the execution transaction.

"Gasless" applies to swap execution. The input token may still require a one-time onchain ERC-20 approval to Permit2.

Prerequisites

  • EVM wallet that supports EIP-712 typed-data signing
  • xPath API key
  • Input ERC-20 balance
  • Source network with gaslessSwaps and permit2 enabled
  • Native gas only if Permit2 approval is still required

Gasless Same-Chain Swap

Request a Gasless Quote

Use acquisition mode 2 for Permit2 or 3 for Permit2 Witness:

const API_URL = 'https://api.xpath.rath.fi'
const API_KEY = process.env.XPATH_API_KEY!
const sender = 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({
  fromToken: '0x4200000000000000000000000000000000000006',
  toToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  amount: '1000000000000000',
  fromChain: '8453',
  toChain: '8453',
  sender,
  receiver: sender,
  acquisitionMode: '2',
  rankingMode: 'balanced',
  slippage: '1',
  deadlineMinutes: '20',
})

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

if (!selected?.quoteId || !selected?.eip712) {
  throw new Error('No complete gasless quote was returned')
}

Check Permit2 Allowance

The EIP-712 domain's verifying contract is the Permit2 contract for the selected quote.

import { erc20Abi } from 'viem'

const inputToken = selected.quote.fromToken.address
const permit2 = selected.eip712.domain.verifyingContract
const requiredAmount = BigInt(selected.eip712.message.permitted.amount)

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

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

Sign the EIP-712 Payload

Sign exactly the payload returned by xPath:

const typedData = selected.eip712

if (Date.now() >= Number(selected.deadline) * 1000) {
  throw new Error('Gasless quote expired; request a new quote')
}

const signature = await walletClient.signTypedData({
  domain: {
    ...typedData.domain,
    chainId: Number(typedData.domain.chainId),
  },
  types: typedData.types,
  primaryType: typedData.primaryType,
  message: typedData.message,
})

Do not change the token, amount, spender, nonce, deadline, witness, or receiver after requesting the quote. Request a new quote instead.

Submit the Signed Swap

const swap = await xpath('/gasless/submit-swap', {
  method: 'POST',
  body: JSON.stringify({
    quoteId: selected.quoteId,
    signature,
  }),
})

console.log('Swap ID:', swap.swapId)
console.log('Tachyon transaction ID:', swap.tachyonTxId)
console.log('Status:', swap.status)

Track Relay Execution

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

let status
while (Date.now() < timeoutAt) {
  status = await xpath(
    `/gasless/status?${new URLSearchParams({ id: swap.swapId })}`
  )

  console.log('Gasless status:', status.status)
  if (terminalStatuses.has(status.status.toLowerCase())) break

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

if (!status || !terminalStatuses.has(status.status.toLowerCase())) {
  throw new Error('Gasless status polling timed out')
}
if (status.status.toLowerCase() === 'failed') throw new Error('Gasless execution failed')
if (!status.executionTxHash) throw new Error('Missing source transaction hash')
console.log('Execution transaction:', status.executionTxHash)

Full Gasless Swap 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(`${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 params = new URLSearchParams({
    fromToken: '0x4200000000000000000000000000000000000006',
    toToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    amount: '1000000000000000',
    fromChain: String(base.id),
    toChain: String(base.id),
    sender,
    receiver: sender,
    acquisitionMode: '2',
    rankingMode: 'balanced',
    slippage: '1',
    deadlineMinutes: '20',
  })

  const quotes = await xpath(`/gasless/quote?${params}`)
  const selected = quotes.find(
    (candidate) => candidate.quote.routeKind === 'sameChainSwap'
  )
  if (!selected) throw new Error('No gasless same-chain route is available')

  console.log('Expected output:', selected.quote.amountOut)
  console.log('Gasless fee:', selected.fee.feeAmount)

  const typedData = selected.eip712
  const inputToken = selected.quote.fromToken.address as `0x${string}`
  const permit2 = typedData.domain.verifyingContract as `0x${string}`
  const requiredAmount = BigInt(typedData.message.permitted.amount)

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

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

  if (Date.now() >= Number(selected.deadline) * 1000) {
    throw new Error('Gasless quote expired; request a new quote')
  }

  const signature = await walletClient.signTypedData({
    domain: {
      ...typedData.domain,
      chainId: Number(typedData.domain.chainId),
    },
    types: typedData.types,
    primaryType: typedData.primaryType,
    message: typedData.message,
  })

  const swap = await xpath('/gasless/submit-swap', {
    method: 'POST',
    body: JSON.stringify({ quoteId: selected.quoteId, signature }),
  })

  console.log('Swap ID:', swap.swapId)
  console.log('Tachyon transaction ID:', swap.tachyonTxId)

  const terminal = new Set(['executed', 'failed'])
  const timeoutAt = Date.now() + 10 * 60 * 1000

  while (Date.now() < timeoutAt) {
    const status = await xpath(
      `/gasless/status?${new URLSearchParams({ id: swap.swapId })}`
    )
    console.log('Status:', status.status)

    if (terminal.has(status.status.toLowerCase())) {
      console.log('Execution transaction:', status.executionTxHash)
      if (status.status.toLowerCase() === 'failed') process.exitCode = 1
      return
    }

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

  throw new Error('Gasless swap status polling timed out')
}

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

Gasless Cross-Chain Swap

A gasless cross-chain swap uses the same Permit2 and Tachyon flow with different source and destination chains. Select a route whose routeKind is directBridge or swapBridge.

Request a Gasless Cross-Chain Quote

const params = new URLSearchParams({
  fromToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  toToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  amount: '1000000',
  fromChain: '8453',
  toChain: '42161',
  sender,
  receiver: sender,
  acquisitionMode: '2',
  rankingMode: 'balanced',
  slippage: '1',
  deadlineMinutes: '20',
})

const quotes = await xpath(`/gasless/quote?${params}`)
const selected = quotes.find((candidate) =>
  ['directBridge', 'swapBridge'].includes(candidate.quote.routeKind)
)

if (!selected) throw new Error('No gasless cross-chain route is available')

Approve Permit2 and Sign

Use the same approval and EIP-712 signing flow as the gasless swap. The permitted amount must come from the returned EIP-712 message:

const typedData = selected.eip712
const requiredAmount = BigInt(typedData.message.permitted.amount)

// Check/approve the input token to typedData.domain.verifyingContract,
// then sign typedData without changing any field.

Submit and Track the Cross-Chain Swap

const swap = await xpath('/gasless/submit-swap', {
  method: 'POST',
  body: JSON.stringify({ quoteId: selected.quoteId, signature }),
})

const relayTimeoutAt = Date.now() + 10 * 60 * 1000
let relayStatus
while (Date.now() < relayTimeoutAt) {
  relayStatus = await xpath(
    `/gasless/status?${new URLSearchParams({ id: swap.swapId })}`
  )
  if (['executed', 'failed'].includes(relayStatus.status)) break
  await new Promise((resolve) => setTimeout(resolve, 3000))
}

if (!relayStatus || !['executed', 'failed'].includes(relayStatus.status)) {
  throw new Error('Relay status polling timed out')
}
if (relayStatus.status === 'failed') throw new Error('Relay execution failed')
if (!relayStatus.executionTxHash) throw new Error('Missing source transaction hash')

console.log('Source transaction:', relayStatus.executionTxHash)

The gasless status reaches executed when Tachyon executes the source-chain transaction. The current gasless status API does not expose a separate destination-completion state, so treat executionTxHash as source execution only and do not report destination completion from this response.

Full Gasless Cross-Chain Swap Script

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(`${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 params = new URLSearchParams({
    fromToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    toToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
    amount: '1000000',
    fromChain: '8453',
    toChain: '42161',
    sender,
    receiver: sender,
    acquisitionMode: '2',
    rankingMode: 'balanced',
    slippage: '1',
    deadlineMinutes: '20',
  })

  const quotes = await xpath(`/gasless/quote?${params}`)
  const selected = quotes.find((candidate) =>
    ['directBridge', 'swapBridge'].includes(candidate.quote.routeKind)
  )
  if (!selected) throw new Error('No gasless cross-chain route is available')

  console.log('Route:', selected.quote.routeKind)
  console.log('Providers:', selected.quote.providers.map((provider) => provider.name))
  console.log('Expected output:', selected.quote.amountOut)
  console.log('Gasless fee:', selected.fee.feeAmount)

  const typedData = selected.eip712
  const token = selected.quote.fromToken.address as `0x${string}`
  const permit2 = typedData.domain.verifyingContract as `0x${string}`
  const requiredAmount = BigInt(typedData.message.permitted.amount)

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

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

  if (Date.now() >= Number(selected.deadline) * 1000) {
    throw new Error('Gasless quote expired; request a new quote')
  }

  const signature = await walletClient.signTypedData({
    domain: {
      ...typedData.domain,
      chainId: Number(typedData.domain.chainId),
    },
    types: typedData.types,
    primaryType: typedData.primaryType,
    message: typedData.message,
  })

  const swap = await xpath('/gasless/submit-swap', {
    method: 'POST',
    body: JSON.stringify({ quoteId: selected.quoteId, signature }),
  })

  console.log('Swap ID:', swap.swapId)
  console.log('Tachyon transaction ID:', swap.tachyonTxId)

  const relayTerminal = new Set(['executed', 'failed'])
  const relayTimeoutAt = Date.now() + 10 * 60 * 1000
  let executionTxHash: `0x${string}` | undefined

  while (Date.now() < relayTimeoutAt) {
    const status = await xpath(
      `/gasless/status?${new URLSearchParams({ id: swap.swapId })}`
    )
    const normalizedStatus = status.status.toLowerCase()
    console.log('Relay status:', status.status)

    if (relayTerminal.has(normalizedStatus)) {
      if (normalizedStatus === 'failed') throw new Error('Relay execution failed')
      if (!status.executionTxHash) throw new Error('Missing source transaction hash')
      executionTxHash = status.executionTxHash
      break
    }

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

  if (!executionTxHash) throw new Error('Relay status polling timed out')

  console.log('Source transaction:', executionTxHash)
  console.log('The current gasless status API reports source execution, not destination completion.')
}

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

Security Checklist

  • Verify the EIP-712 domain chain and verifying contract before prompting the wallet.
  • Display token, amount, receiver, fee, minimum output, and deadline.
  • Reject expired quotes and signatures.
  • Keep API keys in a backend or protected proxy for public applications.
  • Persist quoteId, swapId, and tachyonTxId for support and recovery.
  • Never ask users to sign opaque typed data without showing its effect.

On this page