I built a price-time priority matching engine from scratch, plus the defense logic that stops the exact attack pattern used in the Bitget hack: a small test withdrawal, then compromised admin-backend credentials, then forged mass withdrawals. Both are free right now.
Not for real exchange operators — this is a learning resource for backend developers prepping for systems-design interviews or who want a meaty portfolio project. Matching-engine design is a recurring interview topic at companies like Coinbase and Robinhood.
A minimal matching engine that fills buy/sell orders by price-time priority.
// Minimal price-time priority matching engine
class MatchingEngine {
constructor() {
this.buyBook = []; // bids: price desc, then time asc
this.sellBook = []; // asks: price asc, then time asc
this.fills = [];
}
addOrder(side, price, qty) {
const order = { id: crypto.randomUUID(), side, price, qty, time: Date.now() };
if (side === 'buy') {
this.buyBook.push(order);
this.buyBook.sort((a, b) => b.price - a.price || a.time - b.time);
} else {
this.sellBook.push(order);
this.sellBook.sort((a, b) => a.price - b.price || a.time - b.time);
}
this.match();
return order;
}
match() {
while (this.buyBook.length && this.sellBook.length && this.buyBook[0].price >= this.sellBook[0].price) {
const buy = this.buyBook[0], sell = this.sellBook[0];
const qty = Math.min(buy.qty, sell.qty);
buy.qty -= qty; sell.qty -= qty;
this.fills.push({ price: sell.price, qty, buyId: buy.id, sellId: sell.id, time: Date.now() });
if (buy.qty <= 0) this.buyBook.shift();
if (sell.qty <= 0) this.sellBook.shift();
}
}
}
Defends the Bitget (2026-09) pattern: velocity check + multi-approval + timelock.
// Forged-withdrawal defense — modeled on the Bitget (2026-09) incident
class WithdrawalGuard {
constructor({ velocityWindowMs = 10000, velocityThreshold = 3, timelockMs = 10000 } = {}) {
this.recent = [];
this.velocityWindowMs = velocityWindowMs;
this.velocityThreshold = velocityThreshold;
this.timelockMs = timelockMs;
}
request(amount, approvals = 1) {
const now = Date.now();
this.recent = this.recent.filter(r => now - r.time < this.velocityWindowMs);
this.recent.push({ amount, time: now });
// 1) Velocity check: too many requests in a short window
if (this.recent.length > this.velocityThreshold) {
return { status: 'blocked', reason: 'velocity_check' };
}
// 2) Multi-approval: large amounts can't clear on one admin's authority
const requiredApprovals = amount > 1000 ? 2 : 1;
if (approvals < requiredApprovals) {
return { status: 'pending', reason: 'multi_approval_required' };
}
// 3) Timelock: large withdrawals are delayed, not instant
if (amount > 1000) {
return { status: 'timelocked', releaseAt: now + this.timelockMs };
}
return { status: 'approved' };
}
}
Questions or feedback about this product? Leave your email if you'd like a reply (optional).