Prompt file imported from centuari-labs/backend-v2 (
.github/prompts/plan-websocketGatewayForOrderBook.prompt.md). Copyright stays with the author.
Plan: WebSocket Gateway for Order Book
Create a real-time WebSocket gateway that bridges the matching engine's NATS events with Socket.IO clients, enabling live order book updates, match notifications, and user-specific order status changes.
Steps
-
Enhance websocket.gateway.ts with room management, client subscription handlers (
subscribe-orderbook,unsubscribe-orderbook,subscribe-user-orders), connection/disconnection logging, and authentication viaPrivyGuard -
Create NATS subscription handlers in websocket.gateway.ts using
NatsService.subscribe()formatches.created,orders.status,orderbook.snapshot, andorders.errortopics from the matching engine -
Implement broadcasting logic to emit NATS events to appropriate Socket.IO rooms: orderbook snapshots to
orderbook:{loanToken}:{maturity}rooms, match notifications to relevant rooms, user order updates touser:{accountId}rooms -
Add order book state caching (optional) to store latest snapshots per token/maturity, enabling immediate snapshot delivery to newly subscribed clients without waiting for matching engine updates
-
Create DTO types for WebSocket payloads (OrderBookSnapshotDto, MatchNotificationDto, OrderStatusUpdateDto) to ensure type-safe client-server communication
Further Considerations
-
Authentication Strategy: Should clients authenticate via JWT token in connection handshake, or allow anonymous subscriptions for public order book with authenticated subscriptions only for user-specific orders?
-
Throttling/Batching: Should high-frequency order book updates be debounced/throttled (e.g., max 10 updates/second per room) to prevent overwhelming clients, or send every update in real-time?
-
Initial Snapshot Delivery: Should the gateway request order book snapshots from the matching engine on-demand when clients subscribe, or wait for the next periodic snapshot broadcast?
Research Context
Matching Engine Communication Architecture
Communication Protocol: NATS Message Broker
The matching engine is a separate service that communicates with this backend via NATS pub/sub messaging.
NATS Topics Flow
Backend → Matching Engine (Published by Backend):
orders.lend.market- Lend market ordersorders.lend.limit- Lend limit ordersorders.borrow.market- Borrow market ordersorders.borrow.limit- Borrow limit ordersorders.cancel- Order cancellation requests
Matching Engine → Backend (Should Subscribe to):
matches.created- Match results after order processing (containsorderId,matches[],remainingOrder)orders.status- Order status updatesorderbook.snapshot- Order book snapshotsorders.error- Error notifications with standardized error codes
Current Order Structure & Entities
Order Entity
{
id: string; // UUID
accountId: string; // Foreign key to Account
assetId: string; // Foreign key to Token
side: OrderSide; // "LEND" | "BORROW"
type: OrderType; // "MARKET" | "LIMIT"
rate: number; // Basis points (e.g., 500 = 5%)
quantity: string; // Numeric string
filledQuantity: string; // Numeric string (default 0)
settlementFee: string; // Numeric string
status: OrderStatus; // "OPEN" | "FILLED" | "CANCELLED" | "PARTIALLY_FILLED"
createdAt: Date;
updatedAt: Date;
}
Account Entity
{
id: string;
privyUserId: string;
userWallet: string;
createdAt: Date;
}
Token/Asset Entity
{
id: string;
tokenAddress: string;
symbol: string;
name: string;
imageUrl: string;
isLoanToken: boolean;
LLTV: number; // Liquidation Loan-to-Value
LT: number; // Liquidation Threshold
LP: number; // Liquidation Penalty
createdAt: Date;
updatedAt: Date;
}
NATS Service Capabilities
class NatsService {
// Core methods
publish(subject: string, data: unknown): Promise<void>
subscribe<T>(subject: string, callback: (data: T) => void | Promise<void>): Promise<void>
isConnected(): boolean
getConnection(): NatsConnection | null
// Configuration
- NATS_URL: process.env.NATS_URL || "nats://localhost:4222"
- Auto-reconnect: maxReconnectAttempts: -1 (infinite)
- Reconnect wait: 1000ms
- Connection name: "centuari-backend"
}
Current WebSocket Gateway State
@WebSocketGateway({
cors: { origin: '*' }
})
class EventsGateway {
@WebSocketServer() server: Server;
// Example handlers (not order-book related)
@SubscribeMessage('events')
findAll(@MessageBody() data: any): Observable<WsResponse<number>>
@SubscribeMessage('identity')
async identity(@MessageBody() data: number): Promise<number>
}
Current State:
- ✅ Basic Socket.IO gateway configured
- ✅ CORS enabled (origin: '*')
- ✅ Server instance available for broadcasting
- ❌ No integration with NATS
- ❌ No order book subscriptions
- ❌ No real-time order updates
- ❌ Only has dummy/example handlers
Expected Message Formats
matches.created Response:
{
orderId: string;
matches: Match[];
remainingOrder?: Order; // If partially filled
}
orders.status Response:
{
orderId: string;
status: OrderStatus;
filledQuantity?: string;
}
orderbook.snapshot Response:
{
loanToken: string;
maturity: number;
lendOrders: Order[];
borrowOrders: Order[];
}
orders.error Response:
{
orderId?: string;
errorCode: string; // VALIDATION_ERROR, INVALID_ORDER, etc.
message: string;
}
Error Codes from Matching Engine
VALIDATION_ERROR- Invalid order dataINVALID_ORDER- Business rule violationsORDER_NOT_FOUND- Order doesn't existRATE_MISMATCH- Rate matching issuesINSUFFICIENT_LIQUIDITY- No matching ordersINTERNAL_ERROR- Service errorsNATS_CONNECTION_ERROR- Connection issuesMESSAGE_PARSE_ERROR- JSON parsing failures
Order Book Data Structure
Map<loanToken, Map<maturity, RBTree<Order>>>
└─ Each token has multiple maturities
└─ Each maturity has a sorted tree of orders
└─ Sorted by rate (price) and timestamp
Matching Algorithm Characteristics
- Price-Time Priority: Best price first, then earliest timestamp
- Data Structure: Red-Black Trees (O(log n) operations)
- Order Flow:
- Backend publishes order → NATS
- Matching engine receives → validates → matches
- Matching engine creates matches → updates order book
- Matching engine publishes results → NATS
- Backend should subscribe and process results
