Skip to main content

Command Palette

Search for a command to run...

Building a High-Performance Crypto Matching Engine for Nigerian Exchanges

Updated
5 min read

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