Exchange Integrations
Overview
BlockbotX supports two cryptocurrency exchanges for automated trading:
- Binance -- the largest global exchange by volume
- OKX -- a major exchange with strong derivatives and demo trading support
All exchange API keys are encrypted at rest using AES-256-GCM and stored in the ExchangeConnection database model. The schema enforces @@unique([userId, exchange]) to prevent duplicate connections per user per exchange.
Both exchanges support testnet/demo mode, allowing users to paper-trade without risking real funds. The isTestnet flag on each ExchangeConnection record determines whether live or demo endpoints are used.
Key files:
| Component | Path |
|---|---|
| Binance client | lib/binance/client.ts |
| Binance trading | lib/binance/trading.ts |
| Binance WebSocket | lib/binance/websocket.ts |
| OKX client | lib/okx/client.ts |
| OKX symbol mapping | lib/okx/symbol-map.ts |
| Encryption module | lib/encryption/api-keys.ts |
| Connect API route | app/api/exchange/connect/route.ts |
| Test-connection route | app/api/exchange/test-connection/route.ts |
Binance Integration
Client (lib/binance/client.ts)
Uses the binance-api-node npm package. There are two client types:
Public client (singleton) -- created via getBinanceClient(). This client requires no API keys and is used for Binance-specific public endpoints:
exchangeInfo()-- trading pairs, lot sizes, filters (cached 1 hour)book({ symbol, limit })-- order book depthcandles({ symbol, interval, limit })-- candlestick/kline datatrades({ symbol, limit })-- recent tradesping()-- connectivity test
User client (per-user) -- created via getUserBinanceClient(apiKey, apiSecret, isTestnet). This client is authenticated and used for:
accountInfo()-- account balances and permissionsorder()-- place orders (market, limit, stop-loss)cancelOrder()-- cancel a specific ordergetOrder()-- check order statusopenOrders()-- list open ordersallOrders()-- list all orders for a symbolmyTrades()-- trade historycancelOpenOrders()-- cancel all open orders for a symbol
Testnet support: When BINANCE_TESTNET=true (env) or isTestnet=true (per-user), the client routes to:
- HTTP:
https://demo-api.binance.com(instead ofhttps://api.binance.com) - WebSocket:
wss://demo-stream.binance.com(instead ofwss://stream.binance.com:9443/ws)
Price fetching: The getPrice(), getPrices(), and get24hrTicker() functions do NOT use the Binance API. They use CoinGecko via the PriceOracle (lib/defi/price-oracle.ts) with a 5-second Redis cache. This avoids Binance rate limits for price data that does not require exchange-specific precision.
Mock client for CI/test: When process.env.CI or NODE_ENV=test, getBinanceClient() returns a mock client with realistic static data for all public endpoints. This allows API route tests to verify response structure without live credentials.
Credential retrieval: The private getUserBinanceCredentials(userId) function fetches the user's active Binance ExchangeConnection from the database, decrypts the API key and secret, and returns them along with the isTestnet flag. Higher-level functions like placeBinanceOrder(), cancelBinanceOrder(), getOrderStatus(), and getBinanceBalance() use this internally so callers only need to pass a userId.
Trading (lib/binance/trading.ts)
Provides typed wrappers around Binance order operations:
Order placement:
placeOrder(apiKey, apiSecret, params, isTestnet)
placeMarketOrder(apiKey, apiSecret, symbol, side, quantity?, quoteOrderQty?, isTestnet)
placeLimitOrder(apiKey, apiSecret, symbol, side, quantity, price, timeInForce, isTestnet)
PlaceOrderParamssupports types:MARKET,LIMIT,STOP_LOSS,STOP_LOSS_LIMIT,TAKE_PROFIT,TAKE_PROFIT_LIMIT- Market buy orders can use
quoteOrderQtyto specify the amount in quote asset (e.g., spend 100 USDT) - Limit orders default to
GTC(Good Till Cancelled) time-in-force
Order management:
cancelOrder(apiKey, apiSecret, params, isTestnet)
cancelAllOrders(apiKey, apiSecret, symbol)
getOrderStatus(apiKey, apiSecret, symbol, orderId)
getOpenOrders(apiKey, apiSecret, symbol?)
getAllOrders(apiKey, apiSecret, symbol, limit)
getAccountTrades(apiKey, apiSecret, symbol, limit)
Order fill polling:
waitForOrderFill(apiKey, apiSecret, symbol, orderId, maxAttempts, intervalMs, isTestnet)
// Returns: { filled: boolean; order: BinanceOrderResult }
Polls the order status at intervalMs intervals (default 1000ms) up to maxAttempts (default 10). Returns immediately on terminal statuses: FILLED, CANCELED, REJECTED, EXPIRED.
WebSocket (lib/binance/websocket.ts)
The BinanceWebSocketManager class provides real-time market data streaming via the binance-api-node WebSocket interface. Accessed as a singleton via getBinanceWebSocketManager().
Available subscriptions:
| Method | Data | Returns |
|---|---|---|
subscribeToPriceUpdates(symbol) | Single symbol price | Unsubscribe fn |
subscribeToMultiplePriceUpdates(symbols) | Filtered from all-tickers | Unsubscribe fn |
subscribeToTickerUpdates(symbol) | 24hr ticker for one symbol | Unsubscribe fn |
subscribeToAllTickers() | All trading pair tickers | Unsubscribe fn |
subscribeToKlines(symbol, interval) | Candlestick updates | Unsubscribe fn |
Key interfaces:
PriceUpdate--{ symbol, price, timestamp }TickerUpdate--{ symbol, priceChange, priceChangePercent, weightedAvgPrice, lastPrice, volume, quoteVolume, openTime, closeTime, high, low }KlineUpdate--{ symbol, interval, startTime, endTime, open, high, low, close, volume, isFinal }
Each subscription returns an unsubscribe function. Duplicate subscriptions to the same symbol are prevented. Call unsubscribeAll() to close all active streams.
OKX Integration
Client (lib/okx/client.ts)
Zero-dependency implementation using raw fetch() and Node.js crypto module -- no npm packages required.
Base URL: https://my.okx.com (NOT www.okx.com). The my.okx.com domain is required for demo/simulated trading API keys.
Authentication signing:
sign(timestamp, method, requestPath, body, secretKey) -> base64 string
The signature is computed as: Base64(HMAC-SHA256(timestamp + METHOD + requestPath + body, secretKey))
Auth headers (added to every authenticated request):
| Header | Value |
|---|---|
OK-ACCESS-KEY | API key |
OK-ACCESS-SIGN | HMAC-SHA256 signature (base64) |
OK-ACCESS-TIMESTAMP | ISO 8601 timestamp |
OK-ACCESS-PASSPHRASE | API passphrase |
Demo mode: Uses the same base URL but adds the header x-simulated-trading: 1.
Core request function:
okxRequest<T>(method, path, credentials?, body?) -> Promise<T>
Handles authentication headers, demo-mode header, HTTP error checking, and OKX-specific error code validation (code must be "0" for success).
Public endpoints (no auth needed):
getOkxTickers()-- fetches all SPOT tickers via/api/v5/market/tickers?instType=SPOT. Returns array of{ instId, last, vol24h, high24h, low24h, open24h }.getOkxPrices(symbols?)-- returnsMap<instId, price>. Ifsymbolsarray provided, filters to those; otherwise returns all.
Private endpoints (auth required):
getOkxAccountBalance(credentials)-- fetches account balance via/api/v5/account/balance. Returns array ofOkxBalance({ ccy, availBal, frozenBal, bal }).testOkxConnection(credentials)-- validates credentials by attempting a balance fetch. Returnstrueon success,falseon failure.
Credentials interface:
interface OkxCredentials {
apiKey: string;
apiSecret: string;
passphrase: string; // Required -- OKX always requires a passphrase
isTestnet: boolean;
}
Symbol Format (lib/okx/symbol-map.ts)
Binance and OKX use different symbol formats:
| Exchange | Format | Example |
|---|---|---|
| Binance | Concatenated | BTCUSDT |
| OKX | Hyphen-separated | BTC-USDT |
Conversion functions:
binanceToOkx("BTCUSDT") // returns "BTC-USDT"
okxToBinance("BTC-USDT") // returns "BTCUSDT"
binanceToOkx() splits on known quote assets: USDT, USDC, BUSD, BTC, ETH, EUR, GBP. If the input already contains a hyphen, it is returned as-is.
okxToBinance() simply removes the hyphen.
API Key Encryption
All exchange credentials are encrypted using AES-256-GCM (authenticated encryption) in lib/encryption/api-keys.ts.
Configuration
- Requires a 32-character
ENCRYPTION_KEYenvironment variable - The key is lazily resolved on first use (not at import time), allowing
next buildto proceed without the key in the environment - If the key is missing or not exactly 32 characters, the application throws a fatal error
Encryption Format
Encrypted values are stored as a colon-separated string:
iv:authTag:encryptedData
All three components are hex-encoded:
- IV (Initialization Vector): 12 bytes (96 bits), randomly generated per encryption call via
crypto.randomBytes(12). A unique IV per encryption ensures identical plaintext produces different ciphertext. - Auth Tag: 16 bytes, produced by GCM mode. Provides tamper detection -- if the ciphertext or IV is modified, decryption fails.
- Encrypted Data: The AES-256-GCM ciphertext of the original API key/secret/passphrase.
Functions
encrypt(text: string): string // Returns "iv:authTag:encryptedData"
decrypt(encryptedText: string): string // Parses format and decrypts
hash(text: string): string // SHA-256 one-way hash
verifyHash(text, hashedText): boolean // Constant-time comparison (timing-attack safe)
generateEncryptionKey(length): string // Generate a random key (default 32 chars)
Storage in Database
The ExchangeConnection model stores:
| Field | Description |
|---|---|
encryptedApiKey | Encrypted API key (both exchanges) |
encryptedSecretKey | Encrypted API secret (both exchanges) |
encryptedPassphrase | Encrypted passphrase (OKX only, nullable) |
Keys are decrypted only at the point of use (when making an API call to the exchange) and are never held in memory longer than necessary. The Winston logger's redactFormat automatically filters any field containing key, secret, password, or token to prevent accidental credential exposure in logs.
Connection Flow
1. User Submits Credentials
The user provides their API key, API secret, and (for OKX) passphrase via the UI. They also select whether these are testnet/demo credentials.
2. Test Connection (POST /api/exchange/test-connection)
Before saving, credentials can be tested without persisting them:
- Authentication: Requires authenticated user via
getAuthUser(req). - Validation: Input validated with Zod schema (
exchange,apiKey,apiSecret, optionalpassphrase,isTestnet). - OKX passphrase check: If exchange is
okxand no passphrase is provided, returns a 400 error. - Exchange-specific testing:
- Binance: Calls
testUserConnection()which invokesclient.accountInfo(). - OKX: Calls
testOkxConnection()which invokesgetOkxAccountBalance().
- Binance: Calls
- Balance verification: On successful connection, attempts to fetch balances to verify read permissions. A balance fetch failure does not invalidate the connection.
- Error messages: Provides specific error messages for common failures (invalid API key, signature error, IP whitelist issue, timestamp sync).
3. Save Connection (POST /api/exchange/connect)
Encrypts and persists the credentials:
- Authentication: Requires authenticated user.
- Validation: Same Zod schema as test-connection.
- Live validation: Calls the exchange API to verify credentials before saving (same logic as test-connection).
- Encryption: Encrypts
apiKey,apiSecret, andpassphrase(if present) usingencrypt(). - Upsert logic: Checks for an existing connection with the same
userId+exchange. If found, updates it; otherwise, creates a new record. - Audit logging: Creates an
AuditLogentry forexchange_connectedorexchange_updated.
4. Connection Active
Once saved, the connection is available for bot trading. Bot engines (arbitrage, DCA, etc.) retrieve the user's ExchangeConnection, decrypt the credentials at runtime, and execute trades.
5. Disconnecting (DELETE /api/exchange/connect?id=<connectionId>)
- Verifies the connection belongs to the authenticated user.
- Deletes the
ExchangeConnectionrecord. - Creates an audit log entry for
exchange_disconnected.
6. Listing Connections (GET /api/exchange/connect)
Returns all connections for the authenticated user, excluding encrypted credential fields. Only returns: id, exchange, isActive, createdAt, updatedAt.
Adding a New Exchange
Follow this pattern to integrate a new exchange:
Step 1: Create the Client Module
Create lib/{exchange}/client.ts with:
- An authentication signing function appropriate for the exchange (HMAC-SHA256, RSA, etc.)
- A generic request function that handles auth headers, error codes, and demo-mode switching
- Public market data functions (tickers, prices) -- no auth required
- Private account functions (balance, order placement) -- auth required
- A
testConnection()function that validates credentials by making a lightweight authenticated call - Export a credentials interface specific to the exchange
Reference lib/okx/client.ts for a zero-dependency approach, or use an npm package like lib/binance/client.ts does with binance-api-node.
Step 2: Create the Symbol Map
Create lib/{exchange}/symbol-map.ts with:
- A conversion function from the exchange's symbol format to the internal format (Binance-style concatenated)
- A conversion function from the internal format to the exchange's format
- A list of known quote assets used for splitting pairs
Step 3: Add Exchange to Supported List
Update the Zod validation schemas in the API routes:
app/api/exchange/connect/route.ts-- add the exchange name toz.enum(["binance", "okx", "{new_exchange}"])app/api/exchange/test-connection/route.ts-- same enum update
Step 4: Update Connection Handling
In both API routes, add else if (exchange === "{new_exchange}") branches for:
- Connection testing (calling the new exchange's
testConnection()) - Balance fetching (mapping the exchange's balance format to
{ asset, free, locked })
Step 5: Handle Passphrase or Additional Fields
If the new exchange requires additional credentials (like OKX's passphrase):
- Add validation in the connect route (e.g.,
if (exchange === "{new_exchange}" && !someField)) - Encrypt and store in the appropriate
ExchangeConnectionfield - If the existing schema fields are insufficient, add new encrypted fields to the Drizzle schema in
drizzle/schema/and runpnpm db:generatefollowed bypnpm db:migrate
Step 6: Add Price Fetching to Bot Engines
If the exchange is to participate in arbitrage:
- Update
lib/bot/engine/arbitrage-bot.tsto fetch prices from the new exchange's public API - Use the symbol map to convert between formats when comparing prices across exchanges
Step 7: Update the UI
- Add the new exchange as an option in the connections tab
- Add any exchange-specific fields (e.g., passphrase input) that are conditionally rendered
- Update the exchange icon/logo assets
Step 8: Create a Barrel Export
Create lib/{exchange}/index.ts that re-exports from client.ts, symbol-map.ts, and any other modules. This follows the project convention of barrel exports for all lib domains.