Type/to search
Built-in Functions
Structures
Built-in Variables and Constants
Conventions
Exchanges
Capability Matrix
Centralized Exchanges
Decentralized Exchanges (DEX)
Hyperliquid
Lighter
edgeX
Aster
Aevo
Pendle
WOOFi Pro
Uniswap / PancakeSwap
Aerodrome / Velodrome
KyberSwap
1inch
EVM DEX Common Commands
Jupiter
OKX DEX
DEX Strategy Examples
LP Auto-Rebalancing
Gauge Staking Auto-Compound
DEX-CEX Arbitrage
Jupiter Limit Order Grid
DEX DCA
pump.fun Token Sniping Strategy
Prediction Markets
Securities and Futures
On-chain & Custom

Arbitrages the price spread for the same token between an on-chain DEX and a centralized exchange (CEX) spot market. For each trade size, it gets the executable price on each side and subtracts fees, gas and a safety buffer. If the net profit reaches the threshold, it places one trade on each side.

How It Works
  1. Getting prices: on the DEX side, exchange.IO("route") requests a quote for the per-trade size QTY (one quote for buying and one for selling, using the best route; quotes already include pool fees and price impact). On the CEX side, exchange.GetDepth gets the order book depth, and the strategy simulates filling QTY level by level to work out the average fill price.
  2. The net profit is calculated for each direction. DEX buy → CEX sell = CEX sell proceeds × (1 − CEX_FEE) − DEX buy cost − DEX_GAS_USD. CEX buy → DEX sell = DEX sell proceeds − CEX buy cost × (1 + CEX_FEE) − DEX_GAS_USD.
  3. A trade is placed when all of these are true: the net profit (in bps of trade value), after subtracting the safety buffer SAFETY_BPS, is still at least MIN_NET_BPS; no cooldown is active; both sides hold enough inventory; and the base-currency position is no more than MAX_BASE. The DEX leg goes first, because on-chain transactions can fail or slip, so its limit price allows SLIPPAGE. exchange.GetOrder waits for the swap to be confirmed on-chain and checks the filled amount. Then a limit order for that filled amount is placed on the CEX.
  4. Both sides must hold base and quote currency in advance; the strategy does not make any transfers. Once inventory builds up on one side, it can only wait for an arbitrage opportunity in the opposite direction. No trade is placed if either side runs low on inventory.
  5. Status bar: for each direction, shows the DEX price, CEX average fill price, gross spread, net profit, decision and DEX route. It also shows the inventory on both sides.

Exchange Objects

Add two exchange objects in this order: exchanges[0] is the DEX (Uniswap / PancakeSwap, Aerodrome / Velodrome, Jupiter, etc.), and exchanges[1] is the CEX spot exchange (e.g. Binance). Use the same quote currency on both sides if you can (both default to ETH_USDC). Otherwise you are also exposed to the risk of a stablecoin losing its peg.

Parameters
ParameterDefaultDescription
DEX_SYMBOLETH_USDCDEX trading pair
CEX_SYMBOLETH_USDCCEX trading pair (must be the same token; ideally the same quote currency)
QTY0.01Size of each trade (in base currency)
DEX_GAS_USD0.05Gas cost per DEX swap (USD): about 0.01–0.1 on Base and Arbitrum, about 0.001 on Solana, about 2–10 on Ethereum mainnet
CEX_FEE0.001CEX taker fee rate
SAFETY_BPS10Safety buffer (bps): covers price moves between the quote and the fill
MIN_NET_BPS20Net profit threshold after the safety buffer (bps)
SLIPPAGE0.003Order price protection: the DEX and CEX limit prices each allow 0.3%
MAX_BASE0.1Maximum base-currency position on each side
COOLDOWN_SEC60Cooldown after each arbitrage trade (seconds)
INTERVAL15Check interval (seconds)
PAPER_BASE0.05DRY_RUN paper inventory: starting base currency on each side
PAPER_QUOTE200DRY_RUN paper inventory: starting quote currency on each side
DRY_RUNtrueWhen true, only logs and simulates; no real orders are placed

Risks
  • The two legs are not atomic. After the DEX leg fills, the price may have moved, so the CEX leg fills at a worse price. If the CEX order fails, you are left with one-sided exposure (the log shows it in red, and you must handle it manually).
  • On-chain transactions can be front-run or sandwich-attacked (MEV). They can also revert, which still costs gas. Prices also move while waiting for confirmation.
  • Spreads are usually small. On chains with high gas costs (such as Ethereum mainnet), there are almost no arbitrage opportunities.

DRY_RUN and Live Trading

When DRY_RUN is true (the default), no real orders are placed. The strategy only logs the trades it "would execute". The DEX leg is simulated with exchange.IO("simulate"). Jupiter does not support simulate, so only the quote is checked. The simulation reports an error when the wallet balance is too low; this is expected. Inventory is tracked as paper balances starting from PAPER_BASE and PAPER_QUOTE. Set DRY_RUN to false to trade live. Put inventory on both sides first, and start with a small QTY.

Examples

Full strategy code (with DRY_RUN = true it only logs and simulates, without placing real orders).

javascript
/* * DEX ↔ CEX Spot Arbitrage (Example Strategy) * * Arbitrages the price spread of the same coin between an on-chain DEX and a centralized exchange's spot market: for each trade size QTY, obtains the executable price on both sides: * DEX: the best-route quote from exchange.IO("route", trading pair, "buy"/"sell", QTY) (already includes liquidity pool fees and price impact); * CEX: the average fill price obtained by walking the GetDepth order book level by level for QTY; * Computes the net profit for each direction: * DEX buy → CEX sell: CEX sell proceeds × (1 − CEX_FEE) − DEX buy cost − DEX_GAS_USD * CEX buy → DEX sell: DEX sell proceeds − CEX buy cost × (1 + CEX_FEE) − DEX_GAS_USD * When the net profit (bps, relative to notional) minus the safety buffer SAFETY_BPS is still at least MIN_NET_BPS, one trade is executed on each side: the DEX leg first * (on-chain trades may fail or incur slippage), then, once the fill is confirmed, the CEX leg for the actual filled amount. Both sides must be pre-funded with base and quote currency (the strategy makes no transfers; once inventory * piles up on one side it can only wait for a reverse opportunity), base-currency holdings on each side never exceed MAX_BASE, and no trade is made if either side lacks inventory. * * With DRY_RUN = true (default) no orders are placed: trades that "would have been executed" are only logged, and the DEX leg is simulated with IO("simulate", …) (Jupiter does not support simulate, * quote only; simulation reports an error when the wallet balance is insufficient, which is normal), and inventory is paper-tracked starting from PAPER_*. * * Risks: the two legs are not executed atomically, so the price may have moved after the DEX leg fills, causing the CEX leg to fill at a worse price; on-chain trades may be front-run (MEV), * reverted (only gas is charged), or hit by price changes while waiting for confirmation; when the CEX and DEX quote currencies differ (e.g. USDT vs. USDC) there is also depeg risk, * so by default both sides use the same quote currency. Spreads are usually tiny, and on high-gas chains (such as Ethereum mainnet) there are virtually no opportunities. * * Usage: add a DEX as exchanges[0] (Uniswap, PancakeSwap, Aerodrome, Velodrome, Jupiter, etc.), and as exchanges[1] * add a CEX spot exchange (e.g. Binance); both sides must be funded with base and quote currency. */ // ---------------- Parameters ---------------- var DEX_SYMBOL = "ETH_USDC" // DEX trading pair var CEX_SYMBOL = "ETH_USDC" // CEX trading pair (same coin; ideally the same quote currency) var QTY = 0.01 // Size per trade (base currency) var DEX_GAS_USD = 0.05 // Gas per DEX swap (USD): Base/Arbitrum ~0.01–0.1, Solana ~0.001, Ethereum mainnet 2–10 var CEX_FEE = 0.001 // CEX taker fee rate var SAFETY_BPS = 10 // Safety buffer (bps): covers price movement between quote and fill var MIN_NET_BPS = 20 // Net profit threshold after the safety buffer (bps) var SLIPPAGE = 0.003 // Order price protection: DEX and CEX limit prices each concede 0.3% var MAX_BASE = 0.1 // Max base-currency holding per side var COOLDOWN_SEC = 60 // Cooldown after each arbitrage trade (seconds) var INTERVAL = 15 // Check interval (seconds) var PAPER_BASE = 0.05 // DRY_RUN paper inventory: initial base-currency amount per side var PAPER_QUOTE = 200 // DRY_RUN paper inventory: initial quote-currency amount per side var DRY_RUN = true // true: log and simulate only, no real orders var dex, cex var paper = null // DRY_RUN paper inventory {dex: {base, quote}, cex: {base, quote}} var stats = _G("arb_stats") || { trades: 0, pnl: 0 } var lastTrade = 0 var cexDecimals = 4 // CEX amount precision (read from GetMarkets at startup) function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } // Walk the order book level by level for the given quantity: returns {avg, worst}, or null if depth is insufficient function walk(levels, qty) { var left = qty, cost = 0 for (var i = 0; i < levels.length && left > 1e-12; i++) { var q = Math.min(left, levels[i].Amount) cost += q * levels[i].Price left -= q if (left <= 1e-12) return { avg: cost / qty, worst: levels[i].Price } } return null } function inventory() { if (DRY_RUN) return paper var a = dex.GetAccount(), b = cex.GetAccount() if (!a || !b) throw new Error("GetAccount failed: " + GetLastError()) return { dex: { base: a.Stocks, quote: a.Balance }, cex: { base: b.Stocks, quote: b.Balance } } } // Wait for the DEX swap to be confirmed on-chain; returns the filled base-currency amount (0 on failure) function dexFill(id) { for (var i = 0; i < 60; i++) { var o = dex.GetOrder(id) if (o && o.Status === ORDER_STATE_CLOSED) return o.DealAmount if (o && o.Status === ORDER_STATE_CANCELED) return 0 Sleep(2000) } Log("DEX order", id, "not confirmed after two minutes; CEX leg skipped, please check manually", "#FF0000") return 0 } function execute(op, inv) { var dexSide = op.dir === "A" ? "buy" : "sell", cexSide = op.dir === "A" ? "sell" : "buy" var dexPx = op.dexPrice * (dexSide === "buy" ? 1 + SLIPPAGE : 1 - SLIPPAGE) var cexPx = op.cexWorst * (cexSide === "buy" ? 1 + SLIPPAGE : 1 - SLIPPAGE) Log("Arbitrage", op.name, "qty", QTY, "DEX", dexSide, fmt(op.dexPrice, 4), "CEX", cexSide, fmt(op.cexAvg, 4), "est. net profit", fmt(op.net, 4), "(" + fmt(op.netBps, 1) + " bps)", "#0000FF") lastTrade = Date.now() if (DRY_RUN) { if (dex.GetName() !== "Jupiter") { var sim = dex.IO("simulate", DEX_SYMBOL, dexSide, QTY, dexPx) Log(sim ? "DEX simulate passed: " + sim.route : "DEX simulate failed (normal when the wallet balance is insufficient): " + GetLastError()) } var sign = op.dir === "A" ? 1 : -1 inv.dex.base += sign * QTY; inv.dex.quote -= sign * op.dexPrice * QTY inv.cex.base -= sign * QTY; inv.cex.quote += sign * op.cexAvg * QTY * (1 - sign * CEX_FEE) stats.trades++; stats.pnl += op.net return } var id = dex.CreateOrder(DEX_SYMBOL, dexSide, dexPx, QTY) if (!id) { Log("DEX order failed (CEX leg skipped):", GetLastError(), "#FF0000"); return } var filled = dexFill(id) if (filled <= 0) { Log("DEX leg not filled, CEX leg skipped"); return } var p = Math.pow(10, cexDecimals) var cid = cex.CreateOrder(CEX_SYMBOL, cexSide, cexPx, Math.floor(filled * p) / p) if (!cid) { Log("CEX leg order failed, one-sided exposure", filled, "please handle manually:", GetLastError(), "#FF0000"); return } stats.trades++; stats.pnl += op.net * filled / QTY } function scan() { var buy = dex.IO("route", DEX_SYMBOL, "buy", QTY) var sell = dex.IO("route", DEX_SYMBOL, "sell", QTY) var depth = cex.GetDepth(CEX_SYMBOL) if (!buy || !sell || !depth) throw new Error("Failed to get prices: " + GetLastError()) var bid = walk(depth.Bids, QTY), ask = walk(depth.Asks, QTY) var ops = [] if (buy.price && bid) { // A: DEX buy → CEX sell var netA = bid.avg * QTY * (1 - CEX_FEE) - buy.price * QTY - DEX_GAS_USD ops.push({ dir: "A", name: "DEX buy → CEX sell", dexPrice: buy.price, route: buy.best || buy.route, cexAvg: bid.avg, cexWorst: bid.worst, net: netA, netBps: 0 }) } if (sell.price && ask) { // B: CEX buy → DEX sell var netB = sell.price * QTY - ask.avg * QTY * (1 + CEX_FEE) - DEX_GAS_USD ops.push({ dir: "B", name: "CEX buy → DEX sell", dexPrice: sell.price, route: sell.best || sell.route, cexAvg: ask.avg, cexWorst: ask.worst, net: netB, netBps: 0 }) } var inv = inventory() var rows = ops.map(function (op) { op.netBps = op.net / (op.cexAvg * QTY) * 10000 var gross = (op.dir === "A" ? op.cexAvg / op.dexPrice - 1 : op.dexPrice / op.cexAvg - 1) * 10000 var block = "" if (op.netBps - SAFETY_BPS < MIN_NET_BPS) block = "Below threshold" else if (Date.now() - lastTrade < COOLDOWN_SEC * 1000) block = "Cooling down" else if (op.dir === "A" && (inv.dex.quote < op.dexPrice * QTY * (1 + SLIPPAGE) || inv.cex.base < QTY || inv.dex.base + QTY > MAX_BASE)) block = "Insufficient inventory/over limit" else if (op.dir === "B" && (inv.cex.quote < op.cexAvg * QTY * (1 + CEX_FEE + SLIPPAGE) || inv.dex.base < QTY || inv.cex.base + QTY > MAX_BASE)) block = "Insufficient inventory/over limit" if (!block) { execute(op, inv); block = DRY_RUN ? "Logged (DRY_RUN)" : "Executed" } return [op.name, fmt(op.dexPrice, 4), fmt(op.cexAvg, 4), fmt(gross, 1), fmt(op.net, 4), fmt(op.netBps, 1), block, Array.isArray(op.route) ? op.route.join("+") : op.route] }) _G("arb_stats", stats) var t1 = { type: "table", title: "Spread (per trade " + QTY + ", DEX quote incl. pool fee, net profit after CEX fee and gas " + DEX_GAS_USD + ")", cols: ["Direction", "DEX Price", "CEX Avg Price", "Gross Spread bps", "Net Profit", "Net Profit bps", "Result", "DEX Route"], rows: rows } var t2 = { type: "table", title: "Inventory" + (DRY_RUN ? " (DRY_RUN paper)" : ""), cols: ["", "Base Currency", "Quote Currency"], rows: [ [dex.GetName() + " " + DEX_SYMBOL, fmt(inv.dex.base, 6), fmt(inv.dex.quote, 4)], [cex.GetName() + " " + CEX_SYMBOL, fmt(inv.cex.base, 6), fmt(inv.cex.quote, 4)]] } LogStatus(_D(), "Arbitrage trades", stats.trades, "Cumulative est. net profit", fmt(stats.pnl, 4), "\n`" + JSON.stringify([t1, t2]) + "`") } function main() { if (exchanges.length < 2) throw new Error("Two exchange objects required: exchanges[0] is the DEX, exchanges[1] is the CEX spot exchange") dex = exchanges[0]; cex = exchanges[1] dex.SetCurrency(DEX_SYMBOL); cex.SetCurrency(CEX_SYMBOL) var m = (cex.GetMarkets() || {})[CEX_SYMBOL] if (m) cexDecimals = m.AmountPrecision if (DRY_RUN) paper = { dex: { base: PAPER_BASE, quote: PAPER_QUOTE }, cex: { base: PAPER_BASE, quote: PAPER_QUOTE } } Log("DEX", dex.GetName(), DEX_SYMBOL, "CEX", cex.GetName(), CEX_SYMBOL, DRY_RUN ? "DRY_RUN: log only, no orders" : "Live trading") while (true) { try { scan() } catch (e) { Log("Error in this round:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

See Also