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.
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
| Recipe | Endpoint | What it does |
|---|---|---|
| Quote watcher | GET /sor/quotes | Alert when a pair crosses a target output. |
| Swap script | GET /sor/swap | Approve, simulate and send a swap from a server wallet. |
| Price stream | GET /price/stream (SSE) | Push USD prices to a dashboard without polling. |
| Price-gap scanner | GET /sor/quotes on several chains | Compare 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.
"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:
"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.
"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
errorfield and zero amounts. UsebestQuote, 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
buyAmountMinto 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.