Building a High-Performance Crypto Matching Engine for Nigerian Exchanges
The matching engine is the core of any centralized crypto exchange. This guide covers a production-ready implementation for Nigerian CEX platforms.
Architecture Overview
Client WebSocket → API Gateway → Order Manager
↓
Matching Engine (In-Memory)
↓
┌────────────┴────────────┐
Order Book DB Trade Engine
↓ ↓
PostgreSQL Trade Execution
↓
Wallet Settlement
1. Order Book Data Structure
class OrderBook {
constructor(pair) {
this.pair = pair; // e.g. 'USDT-NGN', 'BTC-USDT'
// Sorted maps for O(log n) insertion and lookup
this.bids = new Map(); // Buy orders: price → [orders]
this.asks = new Map(); // Sell orders: price → [orders]
this.bidPrices = []; // Sorted desc
this.askPrices = []; // Sorted asc
}
addOrder(order) {
const { side, price, quantity, id, userId, timestamp } = order;
const book = side === 'BUY' ? this.bids : this.asks;
if (!book.has(price)) {
book.set(price, []);
this.updateSortedPrices(side, price);
}
book.get(price).push({
id, userId, quantity,
remainingQty: quantity,
timestamp
});
return this.tryMatch();
}
updateSortedPrices(side, price) {
if (side === 'BUY') {
this.bidPrices.push(price);
this.bidPrices.sort((a, b) => b - a); // Desc
} else {
this.askPrices.push(price);
this.askPrices.sort((a, b) => a - b); // Asc
}
}
// Price-time priority matching
tryMatch() {
const trades = [];
while (this.bidPrices.length > 0 && this.askPrices.length > 0) {
const bestBid = this.bidPrices[0];
const bestAsk = this.askPrices[0];
// No match if best bid < best ask
if (bestBid < bestAsk) break;
const bidOrders = this.bids.get(bestBid);
const askOrders = this.asks.get(bestAsk);
const bid = bidOrders[0];
const ask = askOrders[0];
// Execute trade at ask price (maker price)
const tradeQty = Math.min(bid.remainingQty, ask.remainingQty);
const tradePrice = bestAsk;
trades.push({
id: `\({Date.now()}-\){Math.random().toString(36).substr(2,9)}`,
pair: this.pair,
buyOrderId: bid.id,
sellOrderId: ask.id,
buyUserId: bid.userId,
sellUserId: ask.userId,
price: tradePrice,
quantity: tradeQty,
value: tradeQty * tradePrice,
timestamp: Date.now()
});
// Update remaining quantities
bid.remainingQty -= tradeQty;
ask.remainingQty -= tradeQty;
// Remove filled orders
if (bid.remainingQty === 0) {
bidOrders.shift();
if (bidOrders.length === 0) {
this.bids.delete(bestBid);
this.bidPrices.shift();
}
}
if (ask.remainingQty === 0) {
askOrders.shift();
if (askOrders.length === 0) {
this.asks.delete(bestAsk);
this.askPrices.shift();
}
}
}
return trades;
}
getDepth(levels = 20) {
return {
bids: this.bidPrices.slice(0, levels).map(price => ({
price,
quantity: this.bids.get(price)
.reduce((sum, o) => sum + o.remainingQty, 0)
})),
asks: this.askPrices.slice(0, levels).map(price => ({
price,
quantity: this.asks.get(price)
.reduce((sum, o) => sum + o.remainingQty, 0)
}))
};
}
}
2. WebSocket Real-Time Order Book
const WebSocket = require('ws');
const { EventEmitter } = require('events');
class OrderBookBroadcaster extends EventEmitter {
constructor(wss) {
super();
this.wss = wss;
this.subscriptions = new Map(); // pair → Set of ws clients
}
subscribe(ws, pair) {
if (!this.subscriptions.has(pair)) {
this.subscriptions.set(pair, new Set());
}
this.subscriptions.get(pair).add(ws);
ws.on('close', () => {
this.subscriptions.get(pair)?.delete(ws);
});
}
broadcastDepth(pair, depth) {
const clients = this.subscriptions.get(pair);
if (!clients) return;
const message = JSON.stringify({
type: 'DEPTH_UPDATE',
pair,
data: depth,
timestamp: Date.now()
});
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
broadcastTrade(pair, trade) {
const clients = this.subscriptions.get(pair);
if (!clients) return;
const message = JSON.stringify({
type: 'TRADE',
pair,
data: {
price: trade.price,
quantity: trade.quantity,
side: 'buy', // simplified
timestamp: trade.timestamp
}
});
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
}
3. Nigerian Fiat On-Ramp (NGN Deposits)
const axios = require('axios');
class NigerianFiatRamp {
constructor() {
this.paystackKey = process.env.PAYSTACK_SECRET_KEY;
}
// Generate unique NGN deposit account for user
async createDedicatedAccount(userId, email, name) {
const response = await axios.post(
'https://api.paystack.co/dedicated_account',
{
customer: { email, first_name: name.split(' ')[0], last_name: name.split(' ')[1] },
preferred_bank: 'wema-bank', // or 'titan-paystack'
country: 'NG'
},
{ headers: { Authorization: `Bearer ${this.paystackKey}` } }
);
const account = response.data.data;
// Save to user profile
await User.findByIdAndUpdate(userId, {
depositAccount: {
bankName: account.bank.name,
accountNumber: account.account_number,
accountName: account.account_name
}
});
return account;
}
// Handle Paystack webhook for NGN deposits
async handleDepositWebhook(payload) {
if (payload.event !== 'dedicatedaccount.assign.success' &&
payload.event !== 'charge.success') return;
const { amount, customer, reference } = payload.data;
const ngnAmount = amount / 100; // Convert from kobo
// Find user by email
const user = await User.findOne({ email: customer.email });
if (!user) return;
// Credit NGN balance
await Balance.findOneAndUpdate(
{ userId: user._id, currency: 'NGN' },
{ $inc: { available: ngnAmount } },
{ upsert: true }
);
// Notify user via WhatsApp
await sendWhatsApp(
user.phone,
`✅ Deposit confirmed!\n` +
`₦${ngnAmount.toLocaleString()} added to your ZikarelHub account.\n` +
`Reference: ${reference}`
);
}
}
4. CEX Security — Hot/Cold Wallet Split
class WalletManager {
constructor() {
this.hotWalletThreshold = 0.05; // 5% in hot wallet
this.coldWalletThreshold = 0.95; // 95% in cold storage
}
async checkHotWalletBalance(asset) {
const totalUserBalance = await Balance.aggregate([
{ $match: { currency: asset } },
{ \(group: { _id: null, total: { \)sum: '$available' } } }
]);
const total = totalUserBalance[0]?.total || 0;
const hotBalance = await this.getHotWalletBalance(asset);
const hotRatio = hotBalance / total;
if (hotRatio < 0.03) {
// Hot wallet too low — alert ops team
await alertOpsTeam(
`⚠️ ${asset} hot wallet below 3%.\n` +
`Hot: \({hotBalance}\nTotal: \){total}\n` +
`Action required: Transfer from cold storage.`
);
}
if (hotRatio > 0.08) {
// Too much in hot wallet — move to cold
await alertOpsTeam(
`⚠️ ${asset} hot wallet above 8%.\n` +
`Move excess to cold storage immediately.`
);
}
}
}
Nigerian CEX Compliance Checklist
[ ] SEC Nigeria VASP registration initiated
[ ] Tiered KYC — BVN, NIN, document verification
[ ] AML transaction monitoring active
[ ] CBN large transaction reporting (above ₦5M)
[ ] Suspicious Activity Report (SAR) system
[ ] Hot/cold wallet ratio maintained (max 10% hot)
[ ] Multi-signature for cold wallet access
[ ] Regular security penetration testing
[ ] NDPR-compliant user data handling
[ ] Insurance for custodied user funds
ZikarelHub LTD builds centralized crypto exchange infrastructure for Nigerian businesses. zikarelhub.tech/crypto-exchange-development-nigeria
