Cross-Exchange Arbitrage API: A Practical Trading Guide
Learn to automate cross-exchange crypto arbitrage using REST APIs. Covers Binance, OKX, and Bybit authentication, spread detection, order execution, and risk controls with real Python code.
Learn to automate cross-exchange crypto arbitrage using REST APIs. Covers Binance, OKX, and Bybit authentication, spread detection, order execution, and risk controls with real Python code.
Price differences between exchanges are a fact of crypto life. Bitcoin might sit at $65,200 on Binance while trading at $65,380 on OKX — a 0.28% spread that, after fees, still leaves profit on the table. The problem is these windows last seconds, not minutes. Catching them manually is impossible. That's where exchange APIs become your edge: they let you monitor prices, detect spreads, and fire orders faster than any human could.
Cross-exchange arbitrage is deceptively simple in theory: buy an asset where it's cheap, sell it where it's expensive, pocket the difference. In practice, three layers of friction eat your profit: trading fees (typically 0.1% taker on Binance and OKX spot markets), transfer latency if you're moving funds between venues, and order slippage on larger sizes.
The cleanest implementation pre-funds both exchanges. You hold USDT on Bybit and BTC on Binance simultaneously. When BTC is cheaper on Bybit, you buy there and sell on Binance — net flat on BTC, net positive in USDT. No withdrawal delays, no blockchain confirmations mid-trade. This is the setup professional arbitrage desks use, and it's what the code below builds toward.
Realistic spreads worth trading are usually 0.15% to 0.5% after fees. Anything below 0.2% gets eroded by fees unless you have maker-level pricing. Platforms like VoiceOfChain track real-time price divergences across venues and can surface when meaningful spreads emerge — useful when you're running multiple strategies and can't watch every pair manually.
Every major exchange offers two API styles. REST APIs are request-response: you ping an endpoint, get a snapshot back. WebSocket APIs push data continuously — price ticks, order book updates, trade fills. For arbitrage, you want WebSocket feeds for price monitoring (low latency, no rate limit concerns) and REST endpoints for order execution.
Authentication on Binance, Bybit, and OKX all use HMAC-SHA256 signing. You generate a key pair in the exchange dashboard, then sign each request with your secret using SHA256. The exchange verifies the signature server-side. One rule that applies everywhere: never hardcode API keys in source files. Use environment variables.
Start with Binance — its API is the most documented and beginner-friendly. The ticker price endpoint is public (no authentication needed), so it's a good place to verify connectivity before wiring up signed requests for order placement.
import hmac
import hashlib
import time
import requests
API_KEY = "your_binance_api_key"
API_SECRET = "your_binance_api_secret"
def create_signature(query_string: str) -> str:
return hmac.new(
API_SECRET.encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256
).hexdigest()
def get_price_binance(symbol: str) -> float:
url = "https://api.binance.com/api/v3/ticker/price"
resp = requests.get(url, params={"symbol": symbol})
resp.raise_for_status()
return float(resp.json()["price"])
def signed_get(endpoint: str, params: dict) -> dict:
"""Signed GET for account/order endpoints."""
params["timestamp"] = int(time.time() * 1000)
query = "&".join(f"{k}={v}" for k, v in params.items())
params["signature"] = create_signature(query)
headers = {"X-MBX-APIKEY": API_KEY}
resp = requests.get(
f"https://api.binance.com{endpoint}",
params=params,
headers=headers
)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
price = get_price_binance("BTCUSDT")
print(f"BTC/USDT on Binance: ${price:,.2f}")
# Output: BTC/USDT on Binance: $65,432.10
# Example: fetch open orders (signed endpoint)
orders = signed_get("/api/v3/openOrders", {"symbol": "BTCUSDT"})
print(f"Open orders: {len(orders)}")
Now add OKX to the mix. Fetching prices from both exchanges concurrently with asyncio cuts latency roughly in half compared to sequential requests — critical when you're scanning dozens of pairs for spread opportunities.
import asyncio
import aiohttp
async def binance_price(session: aiohttp.ClientSession, symbol: str) -> float:
url = f"https://api.binance.com/api/v3/ticker/price?symbol={symbol}"
async with session.get(url) as r:
data = await r.json()
return float(data["price"])
async def okx_price(session: aiohttp.ClientSession, symbol: str) -> float:
# OKX uses dash format: BTC-USDT instead of BTCUSDT
inst_id = symbol[:-4] + "-" + symbol[-4:]
url = f"https://www.okx.com/api/v5/market/ticker?instId={inst_id}"
async with session.get(url) as r:
data = await r.json()
return float(data["data"][0]["last"])
async def detect_spread(symbol: str = "BTCUSDT") -> float:
async with aiohttp.ClientSession() as session:
binance, okx = await asyncio.gather(
binance_price(session, symbol),
okx_price(session, symbol)
)
spread_pct = abs(binance - okx) / min(binance, okx) * 100
cheaper = "OKX" if okx < binance else "Binance"
pricier = "Binance" if okx < binance else "OKX"
print(f"Binance: ${binance:,.2f} | OKX: ${okx:,.2f}")
print(f"Spread: {spread_pct:.4f}% | Buy {cheaper}, Sell {pricier}")
return spread_pct
if __name__ == "__main__":
asyncio.run(detect_spread("BTCUSDT"))
Order execution is where most arbitrage bots fail in production. The common mistakes: no timeout on the HTTP request (hangs indefinitely when an exchange is slow), no check on exchange-level error codes (the HTTP response is 200 but the order was actually rejected), and no handling for partial fills. Here's a production-ready order function for Bybit spot that covers all three cases.
import json
import time
import hmac
import hashlib
import requests
BYBIT_API_KEY = "your_bybit_key"
BYBIT_API_SECRET = "your_bybit_secret"
BASE_URL = "https://api.bybit.com"
RECV_WINDOW = "5000"
def bybit_sign(timestamp: str, payload: str) -> str:
message = f"{timestamp}{BYBIT_API_KEY}{RECV_WINDOW}{payload}"
return hmac.new(
BYBIT_API_SECRET.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256
).hexdigest()
def place_spot_order(symbol: str, side: str, qty: str) -> dict:
"""
side: "Buy" or "Sell"
qty: string quantity, e.g. "0.001"
"""
endpoint = "/v5/order/create"
timestamp = str(int(time.time() * 1000))
body = {
"category": "spot",
"symbol": symbol,
"side": side,
"orderType": "Market",
"qty": qty
}
payload = json.dumps(body)
headers = {
"X-BAPI-API-KEY": BYBIT_API_KEY,
"X-BAPI-SIGN": bybit_sign(timestamp, payload),
"X-BAPI-TIMESTAMP": timestamp,
"X-BAPI-RECV-WINDOW": RECV_WINDOW,
"Content-Type": "application/json"
}
try:
resp = requests.post(
BASE_URL + endpoint,
headers=headers,
data=payload,
timeout=3 # never block longer than 3 seconds
)
resp.raise_for_status()
result = resp.json()
if result["retCode"] != 0:
raise RuntimeError(f"API error {result['retCode']}: {result['retMsg']}")
return result["result"]
except requests.Timeout:
print("Timeout — price window likely closed, order NOT placed")
return {}
except requests.HTTPError as e:
print(f"HTTP {e.response.status_code} from Bybit")
return {}
except RuntimeError as e:
print(f"Order rejected: {e}")
return {}
if __name__ == "__main__":
order = place_spot_order("BTCUSDT", "Buy", "0.001")
if order:
print(f"Filled: orderId={order.get('orderId')}")
Always test with minimum sizes (0.001 BTC or $10 USDT) before scaling. One logic bug in your order side selection — buying on both Binance and Bybit simultaneously instead of arbitraging — can cost real money before you catch it.
The spread you see in the order book is not the spread you capture. You need to account for taker fees on both legs (0.1% per trade = 0.2% round trip on Binance and OKX at standard tier), slippage on larger orders moving through the book, and API latency between your server and the exchange. Running from a home connection adds 5–15ms of network jitter on top of whatever the exchange reports.
For a spread to be worth trading on Binance and OKX spot with standard 0.1% taker fees, you need at least 0.25% gross spread — that leaves a 0.05% cushion for slippage. If you're a VIP maker on either platform (fees as low as 0.02% on Binance VIP 5+), your breakeven drops to around 0.06%, which opens dramatically more opportunities across altcoin pairs.
VoiceOfChain is useful here as a signal layer — it aggregates real-time market data and can surface unusual spread conditions that suggest a structural arbitrage opportunity rather than just noise. When VoiceOfChain flags a sustained divergence between two venues, that's your cue to check whether the spread clears your fee-adjusted threshold before committing capital.
Cross-exchange arbitrage via API is one of the more accessible algorithmic strategies because the logic is clear: find a spread, buy the cheap side, sell the expensive side. The implementation complexity lives in the details — HMAC authentication, concurrent requests, exchange-level error codes, and risk controls that prevent a single bad trade from wiping a session. The code above gives you a working foundation for Binance, OKX, and Bybit. Pair it with real-time market data from VoiceOfChain to surface divergences as they appear, and you have a system built for actual execution rather than just backtesting.