DevelopersAdvanced

DevTools Quickstart: Build a Swap Bot or Price Dashboard

Hands-on recipes on the ChainBridge API: a quote watcher, a swap script, a price stream over SSE and a simple price-gap scanner across chains.

ChainBridge ResearchUpdated 6 min read

On this page6 sections

Key takeaways

  • Four small scripts on the open ChainBridge API: a quote watcher, a swap script, a live price stream and a cross-chain price-gap scanner.
  • No API key: requests are rate limited per IP, so poll every 15 seconds or more and honour Retry-After.
  • The swap script reads a key from the environment, enforces a minimum output and simulates before it sends.
  • Price gaps between chains are usually smaller than the cost of moving funds; the scanner is a learning tool, not a strategy.

Setup

You need Node 20 or later (or Python 3 with requests for the watcher). There is no account to create. Each recipe is a single file you can run with node file.mjs.

Terminal
mkdir chainbridge-bot && cd chainbridge-bot
npm init -y && npm pkg set type=module
npm install viem   "color:#5c6370"># only needed for the swap script
Recipes in this guide
RecipeEndpointWhat it does
Quote watcherGET /sor/quotesAlert when a pair crosses a target output.
Swap scriptGET /sor/swapApprove, simulate and send a swap from a server wallet.
Price streamGET /price/stream (SSE)Push USD prices to a dashboard without polling.
Price-gap scannerGET /sor/quotes on several chainsCompare the same trade across chains (educational).

All amounts are integers in the token's smallest unit, and native ETH uses the address 0xEeee...EEeE. The API guide covers the full request and response format.

Recipe 1: quote watcher

The watcher asks every live source (0x, KyberSwap, ParaSwap and 0x gasless when a taker is given) for the same quote and logs the best one. It reads netBuyAmount when present: the output after gas, which is what the ranking uses.

"color:#5c6370">// watch.mjs: alert when 1 ETH buys at least TARGET USDC on Base
const API = 'https://www.chainbridge.trading/api'
const TARGET = 2_600 "color:#5c6370">// USDC, example threshold
const params = new URLSearchParams({
  chainId: '8453',
  sellToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
  buyToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  sellAmount: '1000000000000000000',
})

async function check() {
  const res = await fetch(`${API}/sor/quotes?${params}`)
  if (res.status === 429) return Number(res.headers.get('Retry-After') ?? 60) * 1000
  const { success, data, error } = await res.json()
  if (!success) throw new Error(error)

  const best = data.bestQuote
  const usdc = Number(best.netBuyAmount ?? best.buyAmount) / 1e6
  console.log(new Date().toISOString(), best.aggregator, usdc.toFixed(2))
  if (usdc >= TARGET) console.log('Target reached') "color:#5c6370">// send to Slack, Telegram...
  return 15_000
}

for (;;) {
  const wait = await check().catch((e) => (console.error(e.message), 30_000))
  await new Promise((r) => setTimeout(r, wait))
}

Recipe 2: swap script with viem

This script fetches an executable quote, refuses it if the guaranteed output is below your floor, simulates it with eth_call and only then sends it. It runs in dry-run mode until you set DRY_RUN=false.

swap.mjs
"color:#5c6370">// swap.mjs: sell native ETH for USDC on Base, with a floor and a dry-run mode
import { createPublicClient, createWalletClient, http, parseEther, formatUnits } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { base } from 'viem/chains'

const API = 'https://www.chainbridge.trading/api'
const DRY_RUN = process.env.DRY_RUN !== 'false'
const MIN_USDC = 240_000_000n "color:#5c6370">// refuse anything below 240 USDC for 0.1 ETH(example)

const account = privateKeyToAccount(process.env.PRIVATE_KEY) "color:#5c6370">// never hardcode a key
const client = createPublicClient({ chain: base, transport: http(process.env.RPC_URL) })
const wallet = createWalletClient({ account, chain: base, transport: http(process.env.RPC_URL) })

const params = new URLSearchParams({
  chainId: '8453',
  sellToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
  buyToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  sellAmount: parseEther('0.1').toString(),
  takerAddress: account.address,
  slippageBps: '50',
})
const { success, data, error } = await fetch(`${API}/sor/swap?${params}`).then((r) => r.json())
if (!success) throw new Error(error)

const quote = data.bestQuote
if (!quote?.to || !quote?.data) throw new Error('No executable route')
if (BigInt(quote.buyAmountMin) < MIN_USDC) throw new Error('Below floor, not sending')
console.log(`${quote.aggregator}: at least ${formatUnits(BigInt(quote.buyAmountMin), 6)} USDC`)

const tx = { account, to: quote.to, data: quote.data, value: BigInt(quote.value ?? '0') }
await client.call(tx) "color:#5c6370">// reverts here cost nothing
if (DRY_RUN) process.exit(0)

const hash = await wallet.sendTransaction(tx)
const receipt = await client.waitForTransactionReceipt({ hash })
console.log(receipt.status, `https://basescan.org/tx/${hash}`)

Recipe 3: live price stream

/api/price/stream pushes USD prices for up to 20 tokens at the interval you choose (2 to 60 seconds). In a browser, EventSource does the parsing for you. In Node, read the response body and split it into events:

stream.mjs
"color:#5c6370">// stream.mjs: Server-Sent Events in Node with fetch, reconnecting on close
const API = 'https://www.chainbridge.trading/api'
const params = new URLSearchParams({
  chainId: '1',
  tokens: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  interval: '5000',
})

async function listen(onPrices) {
  const res = await fetch(`${API}/price/stream?${params}`, { headers: { Accept: 'text/event-stream' } })
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader()
  let buffer = ''
  for (;;) {
    const { value, done } = await reader.read()
    if (done) return
    buffer += value
    const events = buffer.split('\n\n')
    buffer = events.pop()
    for (const event of events) {
      const line = event.split('\n').find((l) => l.startsWith('data: '))
      if (!line) continue "color:#5c6370">// ": heartbeat" comment
      const msg = JSON.parse(line.slice(6))
      if (msg.type === 'price_update') onPrices(msg.prices, msg.timestamp)
      if (msg.type === 'error') console.warn(msg.message)
    }
  }
}

for (;;) {
  await listen((prices) => console.log(prices)).catch((e) => console.error(e.message))
  await new Promise((r) => setTimeout(r, 3_000)) "color:#5c6370">// the server ends each stream after 30 min
}

Messages carry a type: price_update with prices keyed by lowercase address, error with a message, and timeout when the server closes the stream after 30 minutes. Each IP can keep up to 5 streams open.

Recipe 4: cross-chain price-gap scanner (educational)

The same token often trades at slightly different prices on different chains. This script quotes 1 ETH to USDC on four networks and prints the spread. It is a good way to see how liquidity differs between chains.

gaps.mjs
"color:#5c6370">// gaps.mjs: EDUCATIONAL. Same trade(1 ETH -> USDC) quoted on four chains.
const API = 'https://www.chainbridge.trading/api'
const ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
const CHAINS = {
  Ethereum: { id: 1, usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' },
  Arbitrum: { id: 42161, usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' },
  Base: { id: 8453, usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
  Optimism: { id: 10, usdc: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85' },
}

async function quote(chain) {
  const params = new URLSearchParams({
    chainId: String(chain.id),
    sellToken: ETH,
    buyToken: chain.usdc,
    sellAmount: '1000000000000000000',
  })
  const { success, data } = await fetch(`${API}/sor/quotes?${params}`).then((r) => r.json())
  const best = success ? data.bestQuote : null
  return best ? Number(best.netBuyAmount ?? best.buyAmount) / 1e6 : null
}

const rows = await Promise.all(
  Object.entries(CHAINS).map(async ([name, chain]) => ({ name, usdc: await quote(chain) })),
)
const priced = rows.filter((r) => r.usdc !== null).sort((a, b) => b.usdc - a.usdc)
console.table(priced)

const gap = priced.length > 1 ? priced[0].usdc - priced.at(-1).usdc : 0
console.log(`Raw gap: ${gap.toFixed(2)} USDC per ETH, before bridge fees, bridge time and slippage`)

To price the move itself, add a call to /api/crosschain/quote and subtract its gasUSD and fees. Cross-chain trading explains those costs.

Before you run it for real

  • Handle failures per source. A quote response lists failed sources with an error field and zero amounts. Use bestQuote, or filter them out.
  • Back off on 429. Wait for the number of seconds in Retry-After; retrying immediately only extends the block.
  • Re-quote before every swap. Calldata is tied to the price at request time. Never replay an old transaction.
  • Keep a floor. Compare buyAmountMin to your own minimum, independent of the slippage you request.
  • Use your own RPC. Public endpoints are rate limited too; a dedicated RPC makes simulation and receipts reliable.
  • Log, don't guess. Record the aggregator, amounts and transaction hash for every swap.

AI agents can reach the same data through the MCP endpoint, POST /api/mcp.

Need every parameter?

The API reference lists every endpoint, field and error code behind these recipes.

Open developer docs