Skip to main content

Forsyt Orderbook Integration Guide

This guide covers how to integrate with the Forsyt Universal Orderbook from your TypeScript backend.

Table of Contents​

  1. Quick Start
  2. Server Modes
  3. Monitor Dashboard
  4. WebSocket Event Subscription
  5. Core Concepts: BACK vs LAY
  6. Decimal Odds
  7. Market Types (including Asian Handicaps)
  8. Synthetic Matching
  9. Order Types: LIMIT and MARKET
  10. TypeScript gRPC Integration
  11. gRPC Event Streaming
  12. Best Practices
  13. Resetting the Orderbook
  14. Known Issues

Quick Start​

Starting the Server​

# Production mode (monitoring only)
cargo run --bin server

# Test mode (full test UI + stress testing)
cargo run --bin server -- --test

The server exposes:

  • gRPC API on port 50051 (configurable with --grpc-port)
  • HTTP/WebSocket on port 3000 (configurable with --http-port)

Minimal TypeScript Example​

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';

const packageDefinition = protoLoader.loadSync('proto/orderbook.proto');
const proto = grpc.loadPackageDefinition(packageDefinition) as any;

const client = new proto.orderbook.v1.OrderBookService(
'localhost:50051',
grpc.credentials.createInsecure()
);

// 1. Create a market
client.CreateMarket({
market_id: 'match_123',
outcomes: ['Home', 'Away']
}, callback);

// 2. Open the market
client.OpenMarket({ market_id: 'match_123' }, callback);

// 3. Submit a BACK order at 2.50 decimal odds
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
price: 2.50,
price_type: 1, // DECIMAL_ODDS
quantity: 100,
order_type: 'LIMIT'
}, (err: Error | null, response: { order_id: number }) => {
console.log('Order ID:', response.order_id);
});

Server Modes​

The orderbook server supports two operational modes:

Production Mode (Default)​

cargo run --bin server

Enabled Features:

  • ✅ gRPC API for order submission, cancellation, market management
  • ✅ Live Monitor Dashboard at http://localhost:3000/
  • ✅ WebSocket event streaming at ws://localhost:3000/ws
  • ✅ Read-only orderbook API endpoints
  • ❌ Test UI (disabled)
  • ❌ Order submission via HTTP (disabled - use gRPC)
  • ❌ Stress testing (disabled)

Test Mode​

cargo run --bin server -- --test

Enabled Features (everything in Production plus):

  • ✅ Full Test UI at http://localhost:3000/test
  • ✅ HTTP endpoints for order submission (POST /api/orders)
  • ✅ Market management via HTTP (POST /api/markets)
  • ✅ Scenario runner for predefined test cases
  • ✅ Stress testing controls (/api/stress/*)
  • ✅ Reset functionality (POST /api/reset)

Command Line Options​

OptionDefaultDescription
--testfalseEnable test mode with full Test UI
--grpc-port50051Port for gRPC API
--http-port3000Port for HTTP/WebSocket server
# Custom ports example
cargo run --bin server -- --test --grpc-port 50052 --http-port 8080

Monitor Dashboard​

The Monitor Dashboard provides real-time visibility into orderbook activity.

Accessing the Dashboard​

  • URL: http://localhost:3000/ (available in both Production and Test modes)
  • Test UI Link: When in test mode, a banner appears linking to /test

Dashboard Features​

Main Dashboard Tab​

ComponentDescription
Total OrdersRunning count of all orders submitted
MatchesNumber of trades executed
VolumeTotal matched quantity
Resting OrdersOrders waiting on the book
Active MarketsMarkets currently open for trading
Live Order FlowReal-time table of incoming orders with status
Match FeedReal-time trade executions with synthetic match details
Throughput ChartOrders per second graph
Market DepthAggregated bid/ask visualization
Performance MetricsOrders/sec, Matches/sec, Fill Rate

Event Log Tab​

FeatureDescription
All EventsComplete chronological log
FiltersFilter by Orders, Matches, Info, Errors
Export JSONDownload events as JSON
Export CSVDownload events as CSV
Copy AllCopy events to clipboard

Dashboard WebSocket Connection​

The dashboard connects automatically to ws://localhost:3000/ws and displays:

  • Connection status (Connected/Disconnected)
  • Server uptime
  • Events per second rate

WebSocket Event Subscription​

The Forsyt frontend can subscribe to real-time orderbook events via WebSocket to keep the UI updated.

Connecting to WebSocket​

// TypeScript/JavaScript WebSocket client
const ws = new WebSocket('ws://localhost:3000/ws');

ws.onopen = () => {
console.log('Connected to orderbook');
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleOrderbookEvent(data);
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

ws.onclose = () => {
console.log('Disconnected from orderbook');
// Implement reconnection logic
};

Event Types​

The WebSocket sends JSON events with a type field. All events use snake_case naming.

1. Order Submission Event (order_submit)​

Fired when a new order is submitted to the orderbook.

{
"type": "order_submit",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home",
"side": "back",
"price": 400000,
"size": 10000,
"user_id": "user_abc",
"expiry_ms": 1703808000000
}
FieldTypeDescription
order_idnumberUnique order ID assigned by engine
market_idstringMarket identifier
outcome_idstringOutcome being bet on
sidestring"back" or "lay"
pricenumberPrice in basis points (400000 = 40% = 2.50 odds)
sizenumberOrder size in smallest currency unit
user_idstringUser who placed the order
expiry_msnumber?Optional. Expiry timestamp in milliseconds since epoch

2. Match Event (match)​

Fired when two orders are matched (trade executed).

{
"type": "match",
"sequence": 15,
"market_id": "match_123",
"outcome_id": "Home",
"maker_order_id": 38,
"taker_order_id": 42,
"maker_side": "Long",
"taker_side": "Short",
"price": 400000,
"quantity": 5000,
"timestamp": 1703807940000000000,
"is_synthetic": false,
"is_cross_match": false
}
FieldTypeDescription
sequencenumberGlobal sequence number for ordering
market_idstringMarket identifier
outcome_idstringOutcome that was matched
maker_order_idnumberThe resting (maker) order ID
taker_order_idnumberThe incoming (taker) order ID
maker_sidestring"Long" (Back) or "Short" (Lay)
taker_sidestring"Long" (Back) or "Short" (Lay)
pricenumberExecution price in basis points
quantitynumberMatched quantity
timestampnumberUnix timestamp in nanoseconds
is_syntheticbooleanTrue if multi-outcome synthetic match (e.g., Yes/No market)
is_cross_matchbooleanTrue if cross-line match (e.g., Team1 -0.5 ↔ Team2 +0.5)

3. Order Filled Event (order_filled)​

Fired when an order is completely filled (remaining quantity = 0).

{
"type": "order_filled",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
FieldTypeDescription
order_idnumberThe order that was fully filled
market_idstringMarket identifier
outcome_idstringOutcome identifier

4. Order Expired Event (order_expired)​

Fired when an order expires due to its TTL (time-to-live) being reached.

{
"type": "order_expired",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
FieldTypeDescription
order_idnumberThe order that expired
market_idstringMarket identifier
outcome_idstringOutcome identifier

5. Order Cancelled Event (order_cancelled)​

Fired when an order is cancelled (by user request or system).

{
"type": "order_cancelled",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
FieldTypeDescription
order_idnumberThe order that was cancelled
market_idstringMarket identifier
outcome_idstringOutcome identifier

6. Market Created Event (market_created)​

Fired when a new market is created.

{
"type": "market_created",
"market_id": "match_123",
"outcomes": ["Home", "Draw", "Away"],
"synthetic_enabled": true
}
FieldTypeDescription
market_idstringMarket identifier
outcomesstring[]List of outcome names
synthetic_enabledbooleanWhether synthetic matching is enabled

Complete Integration Example​

interface OrderSubmitEvent {
type: 'order_submit';
order_id: number;
market_id: string;
outcome_id: string;
side: 'back' | 'lay';
price: number;
size: number;
user_id: string;
expiry_ms?: number;
}

interface MatchEvent {
type: 'match';
sequence: number;
market_id: string;
outcome_id: string;
maker_order_id: number;
taker_order_id: number;
maker_side: 'Long' | 'Short';
taker_side: 'Long' | 'Short';
price: number;
quantity: number;
timestamp: number;
is_synthetic: boolean;
is_cross_match: boolean;
}

interface OrderFilledEvent {
type: 'order_filled';
order_id: number;
market_id: string;
outcome_id: string;
}

interface OrderExpiredEvent {
type: 'order_expired';
order_id: number;
market_id: string;
outcome_id: string;
}

interface OrderCancelledEvent {
type: 'order_cancelled';
order_id: number;
market_id: string;
outcome_id: string;
}

interface MarketCreatedEvent {
type: 'market_created';
market_id: string;
outcomes: string[];
synthetic_enabled: boolean;
}

type OrderbookEvent =
| OrderSubmitEvent
| MatchEvent
| OrderFilledEvent
| OrderExpiredEvent
| OrderCancelledEvent
| MarketCreatedEvent;

class OrderbookWebSocket {
private ws: WebSocket | null = null;
private reconnectInterval = 3000;

constructor(
private url: string = 'ws://localhost:3000/ws',
private onEvent: (event: OrderbookEvent) => void
) {}

connect() {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('✅ Connected to orderbook WebSocket');
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as OrderbookEvent;
this.onEvent(data);
} catch (e) {
console.error('Failed to parse event:', e);
}
};

this.ws.onclose = () => {
console.log('❌ Disconnected, reconnecting...');
setTimeout(() => this.connect(), this.reconnectInterval);
};

this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}

disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}

// Usage in Forsyt frontend
const orderbookWs = new OrderbookWebSocket('ws://localhost:3000/ws', (event) => {
switch (event.type) {
case 'order_submit':
// Update order list in UI, track for expiry
updateOrderList(event);
if (event.expiry_ms) {
trackOrderExpiry(event.order_id, event.expiry_ms);
}
break;
case 'match':
// Show trade notification, update positions
showTradeNotification(event);
updateUserPositions(event);
break;
case 'order_filled':
// Remove from active orders, update UI
removeFromActiveOrders(event.order_id);
break;
case 'order_expired':
// Show expiry notification, update UI
showExpiryNotification(event.order_id);
removeFromActiveOrders(event.order_id);
break;
case 'order_cancelled':
// Remove from active orders
removeFromActiveOrders(event.order_id);
break;
case 'market_created':
// Add market to available markets list
addMarket(event.market_id, event.outcomes);
break;
}
});

orderbookWs.connect();

Converting Prices for Display​

// Basis points to decimal odds
function bpToDecimalOdds(bp: number): number {
return 1_000_000 / bp;
}

// Example: 400000 BP → 2.50 decimal odds
const displayOdds = bpToDecimalOdds(400000); // 2.50

// Decimal odds to basis points
function decimalOddsToBp(odds: number): number {
return Math.round(1_000_000 / odds);
}

Core Concepts: BACK vs LAY​

In a betting exchange, every bet has two sides:

SideDescriptionWhat You Are Saying
BACKBetting FOR an outcome"I think Team A will win"
LAYBetting AGAINST an outcome"I think Team A will NOT win"

How Matching Works​

A BACK order matches with a LAY order at the same or better odds:

Punter A: BACK Team A @ 2.50 (bets £100 to win £150 profit)
Punter B: LAY Team A @ 2.50 (accepts £150 liability to win £100)
→ Match! Both filled.

Liability Calculation​

SideStakeOddsWin ProfitLose Amount
BACK£1002.50£150£100
LAY£1002.50£100£150 (liability)

LAY Liability Formula: Stake × (Odds - 1)

At 2.50 odds: £100 × 1.50 = £150 liability


Decimal Odds​

The orderbook uses decimal odds (European format) as the standard representation.

Understanding Decimal Odds​

Decimal OddsImplied ProbabilityMeaning
1.5066.7%Strong favourite
2.0050.0%Even chance
2.5040.0%Slight underdog
3.0033.3%Underdog
5.0020.0%Long shot
10.0010.0%Very unlikely

Conversion Formulas​

// Decimal odds to probability
const probability = 1 / decimalOdds;
// Example: 2.50 → 1/2.50 = 0.40 (40%)

// Probability to decimal odds
const decimalOdds = 1 / probability;
// Example: 0.40 → 1/0.40 = 2.50

Internal BasisPoints Representation​

The engine stores prices internally as BasisPoints where 1.0 probability = 1,000,000 BP:

// Decimal odds to BasisPoints
const basisPoints = Math.round((1 / decimalOdds) * 1_000_000);

// Examples:
// 2.50 → 400,000 BP (40%)
// 2.00 → 500,000 BP (50%)
// 1.50 → 666,667 BP (66.67%)

Note: When using price_type: 1 (DECIMAL_ODDS), this conversion is done automatically by the engine.


Market Types​

Binary Market (Two Outcomes)​

The simplest market with two mutually exclusive outcomes:

// Create a binary market
client.CreateMarket({
market_id: 'match_winner',
outcomes: ['Team A', 'Team B']
}, callback);

1X2 Market (Three Outcomes)​

Standard football/soccer match result market:

// Create 1X2 market with synthetic matching enabled
client.CreateMarket({
market_id: 'match_result',
outcomes: ['Home', 'Draw', 'Away']
}, callback);

Multi-Runner Market (Horse Racing, Elections)​

Markets with many mutually exclusive outcomes:

// Create a horse race market
const runners = ['Horse 1', 'Horse 2', 'Horse 3', 'Horse 4', 'Horse 5'];
client.CreateMarket({
market_id: 'race_winner',
outcomes: runners
}, callback);

Asian Handicap Markets​

Asian Handicaps give one team a goal advantage/disadvantage to level the playing field.

Line Types​

Line TypeExamplesSettlement Outcomes
Half-Goal-0.5, -1.5, +0.5, +1.5WIN or LOSE only
Whole-Goal0, -1, -2, +1, +2WIN, LOSE, or PUSH
Quarter-Goal-0.25, -0.75, +1.25WIN, LOSE, HALF-WIN, or HALF-LOSE

How Quarter-Goal Lines Work​

Quarter-goal lines (e.g., -0.25) are automatically split into two half-stakes on adjacent lines:

BACK Team A -0.25 @ 1.90 for £100 becomes:
├── £50 on Team A -0.0 @ 1.90
└── £50 on Team A -0.5 @ 1.90

This is handled automatically by the engine.

Creating Asian Handicap Markets​

// Create Asian Handicap market with standard lines
client.CreateMarket({
market_id: 'match_ah',
outcomes: [
'Home_0.0', // Whole goal (Draw No Bet)
'Home_-0.5', // Half goal
'Home_-1.0', // Whole goal
'Home_-1.5', // Half goal
'Home_-0.25', // Quarter (splits to 0.0 and -0.5)
'Home_-0.75', // Quarter (splits to -0.5 and -1.0)
]
}, callback);

Outcome Naming Convention​

Use the format: Team_Handicap where handicap is the numeric value:

Outcome IDMeaning
Home_-0.5Home team with -0.5 handicap
Away_+0.5Away team with +0.5 handicap
Home_-1.25Home team with -1.25 handicap (quarter line)

Placing Asian Handicap Bets​

// BACK Home -0.5 at 1.90 odds
client.SubmitOrder({
market_id: 'match_ah',
outcome_id: 'Home_-0.5',
side: 'BACK',
price: 1.90,
price_type: 1,
quantity: 10000, // £100
order_type: 'LIMIT',
user_id: 'user_123'
}, callback);

// BACK a quarter line (automatically splits)
client.SubmitOrder({
market_id: 'match_ah',
outcome_id: 'Home_-0.25', // Splits into -0.0 and -0.5
side: 'BACK',
price: 1.90,
price_type: 1,
quantity: 10000, // £50 on each leg
order_type: 'LIMIT',
user_id: 'user_123'
}, callback);

Settlement Outcomes​

ScoreHome -0.5Home -1.0Home -0.25
Home 2-0WINWINWIN
Home 1-0WINPUSHWIN (full)
Draw 0-0LOSELOSEHALF-LOSE
Away 1-0LOSELOSELOSE

Market Lifecycle​

Markets progress through these states:

Created → Open → Suspended → Closed → Settled
// Open market (required before accepting bets)
client.OpenMarket({ market_id: 'match_123' }, callback);

// Suspend during dangerous play or half-time
client.SuspendMarket({ market_id: 'match_123' }, callback);

// Resume trading
client.OpenMarket({ market_id: 'match_123' }, callback);

// Close for settlement
client.CloseMarket({ market_id: 'match_123' }, callback);

Orders can only be submitted when the market is in the Open state.


Synthetic Matching​

Synthetic matching creates implied liquidity by recognising that outcomes in a market are related.

How It Works​

In a 1X2 market (Home/Draw/Away), these bets are mathematically equivalent:

  • BACK Home = LAY Draw + LAY Away combined
  • LAY Home = BACK Draw + BACK Away combined

Example​

Market: Liverpool vs Arsenal (1X2)

Existing orders:
- LAY Draw @ 3.50 (£100)
- LAY Away @ 4.00 (£100)

Incoming order:
- BACK Home @ 2.00 (£100)

Without synthetic matching: No match (no direct BACK/LAY pair)
With synthetic matching: Engine creates implied LAY Home from the two LAY orders

When Synthetic Matching Is Used​

  • 1X2 markets (football/soccer match result)
  • Multi-runner markets (horse racing, elections)

For binary markets (2 outcomes), synthetic matching is simpler:

  • BACK Outcome A @ 2.00 = LAY Outcome B @ 2.00

Order Types: LIMIT and MARKET​

The orderbook supports two order types:

LIMIT Orders​

A LIMIT order specifies both quantity and price. It will:

  1. Match immediately against existing orders at the same or better price
  2. Place any remaining quantity on the book at the specified price
// BACK at 2.50 or better odds
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
price: 2.50, // Will match at 2.50 or higher
price_type: 1, // DECIMAL_ODDS
quantity: 100,
order_type: 'LIMIT',
user_id: 'user_abc123' // For self-trade prevention
}, callback);

MARKET Orders​

A MARKET order matches at the best available price until filled or liquidity exhausted:

// BACK at whatever price is available
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
quantity: 100,
order_type: 'MARKET',
user_id: 'user_abc123'
}, callback);

Warning: MARKET orders may execute at unfavourable prices in thin markets.

Order Parameters Reference​

FieldTypeRequiredDescription
market_idstringYesUnique market identifier
outcome_idstringYesWhich outcome to bet on
sidestringYes"BACK" or "LAY"
pricenumberLIMIT onlyDecimal odds (e.g., 2.50)
price_typenumberLIMIT only1 for DECIMAL_ODDS
quantitynumberYesStake in smallest currency unit
order_typestringYes"LIMIT" or "MARKET"
user_idstringOptionalFor self-trade prevention
client_order_idstringOptionalYour idempotency key

Self-Trade Prevention​

When user_id is provided, the engine prevents matching against the same user's orders:

// User's BACK order
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
price: 2.50,
price_type: 1,
quantity: 100,
order_type: 'LIMIT',
user_id: 'user_abc123' // Same user
}, callback);

// User's LAY order - will NOT match their own BACK
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'LAY',
price: 2.50,
price_type: 1,
quantity: 100,
order_type: 'LIMIT',
user_id: 'user_abc123' // Same user - prevented from self-trade
}, callback);

TypeScript gRPC Integration​

Setup with @grpc/grpc-js​

npm install @grpc/grpc-js @grpc/proto-loader

Complete Client Implementation​

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { promisify } from 'util';

// Load proto
const PROTO_PATH = './proto/orderbook.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});

const proto = grpc.loadPackageDefinition(packageDefinition) as any;

class OrderbookClient {
private client: any;

constructor(address: string = 'localhost:50051') {
this.client = new proto.orderbook.v1.OrderBookService(
address,
grpc.credentials.createInsecure()
);
}

async createMarket(marketId: string, outcomes: string[]): Promise<void> {
return new Promise((resolve, reject) => {
this.client.CreateMarket({ market_id: marketId, outcomes },
(err: Error | null) => err ? reject(err) : resolve());
});
}

async openMarket(marketId: string): Promise<void> {
return new Promise((resolve, reject) => {
this.client.OpenMarket({ market_id: marketId },
(err: Error | null) => err ? reject(err) : resolve());
});
}

async submitBackOrder(
marketId: string,
outcomeId: string,
odds: number,
stake: number,
userId?: string
): Promise<{ order_id: number; status: string }> {
return new Promise((resolve, reject) => {
this.client.SubmitOrder({
market_id: marketId,
outcome_id: outcomeId,
side: 'BACK',
price: odds,
price_type: 1, // DECIMAL_ODDS
quantity: stake,
order_type: 'LIMIT',
user_id: userId
}, (err: Error | null, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}

async submitLayOrder(
marketId: string,
outcomeId: string,
odds: number,
stake: number,
userId?: string
): Promise<{ order_id: number; status: string }> {
return new Promise((resolve, reject) => {
this.client.SubmitOrder({
market_id: marketId,
outcome_id: outcomeId,
side: 'LAY',
price: odds,
price_type: 1,
quantity: stake,
order_type: 'LIMIT',
user_id: userId
}, (err: Error | null, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}

async cancelOrder(marketId: string, orderId: number): Promise<void> {
return new Promise((resolve, reject) => {
this.client.CancelOrder({ market_id: marketId, order_id: orderId },
(err: Error | null) => err ? reject(err) : resolve());
});
}

async getOrderBook(marketId: string, outcomeId: string): Promise<{
bids: Array<{ price: number; quantity: number; order_count: number }>;
asks: Array<{ price: number; quantity: number; order_count: number }>;
}> {
return new Promise((resolve, reject) => {
this.client.GetOrderBook({ market_id: marketId, outcome_id: outcomeId },
(err: Error | null, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}
}

// Usage
async function main() {
const client = new OrderbookClient();

// Create and open market
await client.createMarket('pl_match_1', ['Liverpool', 'Draw', 'Man City']);
await client.openMarket('pl_match_1');

// Place a BACK bet on Liverpool at 2.50 odds, £100 stake
const result = await client.submitBackOrder(
'pl_match_1',
'Liverpool',
2.50,
10000, // £100.00 in pence
'user_123'
);
console.log('Order placed:', result.order_id);

// Check the order book
const book = await client.getOrderBook('pl_match_1', 'Liverpool');
console.log('Best BACK odds available:', book.asks[0]?.price);
console.log('Best LAY odds available:', book.bids[0]?.price);
}

gRPC Event Streaming​

In addition to WebSocket events (see WebSocket Event Subscription), you can subscribe to real-time market events using gRPC server streaming:

// Subscribe to market events
const stream = client.SubscribeMarket({ market_id: 'pl_match_1' });

stream.on('data', (update: any) => {
if (update.match_event) {
const match = update.match_event;
console.log(`Trade: ${match.quantity} @ ${match.price} BP`);
console.log(` Maker Order: ${match.maker_order_id}`);
console.log(` Taker Order: ${match.taker_order_id}`);
}

if (update.order_update) {
const order = update.order_update;
console.log(`Order ${order.order_id}: ${order.status}`);
}

if (update.book_update) {
const book = update.book_update;
console.log(`Book updated: ${book.outcome_id}`);
}
});

stream.on('error', (err: Error) => {
console.error('Stream error:', err);
});

stream.on('end', () => {
console.log('Stream ended');
});

Event Types​

EventDescription
match_eventA trade occurred between two orders
order_updateOrder status changed (filled, partially filled, cancelled)
book_updateOrder book levels changed

Recovery & Journaling​

The engine uses event sourcing for durability. All operations are journaled before acknowledgment.

What Gets Journaled​

  • Market creation and lifecycle changes
  • All order submissions
  • All cancellations
  • Match events

Recovery Process​

On restart, the engine:

  1. Reads the journal from data/journal.log
  2. Replays all events in order
  3. Reconstructs exact state before the shutdown

No data is lost even on unexpected shutdown.


Best Practices​

1. Always Use Decimal Odds​

Set price_type: 1 for all orders to use human-readable decimal odds:

{
price: 2.50,
price_type: 1 // DECIMAL_ODDS
}

2. Include User ID for Self-Trade Prevention​

Always pass a user_id to prevent users from accidentally matching against themselves:

{
user_id: 'unique_user_identifier'
}

3. Use Client Order IDs for Idempotency​

Pass a unique client_order_id to safely retry failed requests:

{
client_order_id: 'uuid-v4-string'
}

4. Handle Market States​

Always check market state before submitting orders:

// Orders will be rejected if market is not OPEN
try {
await client.submitBackOrder(...);
} catch (err: any) {
if (err.code === grpc.status.FAILED_PRECONDITION) {
console.log('Market is not open for trading');
}
}

5. Monitor the Order Book​

Before placing large orders, check available liquidity:

const book = await client.getOrderBook(marketId, outcomeId);
const totalBackLiquidity = book.asks.reduce((sum, level) =>
sum + level.quantity, 0);
console.log('Available to BACK:', totalBackLiquidity);

6. Use LIMIT Orders for Better Control​

Prefer LIMIT orders to avoid slippage:

// Good: LIMIT order with explicit price
{ order_type: 'LIMIT', price: 2.50 }

// Risky: MARKET order may fill at any price
{ order_type: 'MARKET' }

Troubleshooting​

Common Errors​

Error CodeMeaningSolution
FAILED_PRECONDITIONMarket not openCall OpenMarket first
NOT_FOUNDMarket or outcome doesn't existCheck IDs
INVALID_ARGUMENTBad price, quantity, or sideValidate inputs

Checking Connectivity​

// Simple health check
client.GetOrderBook({
market_id: 'test',
outcome_id: 'test'
}, (err: Error | null) => {
if (err && err.message.includes('NOT_FOUND')) {
console.log('Connected successfully (market just does not exist)');
} else if (err) {
console.log('Connection failed:', err.message);
}
});

Resetting the Orderbook​

To start with a completely fresh orderbook (clear all markets, orders, and journal):

The engine provides a Reset command that clears everything in memory and truncates the journal:

// Reset everything via the test UI server (if running)
const response = await fetch('http://localhost:3000/reset', {
method: 'POST'
});

Or using the engine message directly (if you have access to the engine channel):

// This is internal - use the API endpoint above for external access
engineTx.send({
type: 'Reset',
reply_tx: replyChannel
});

Option 2: Delete Journal File (Clean Restart)​

For a complete fresh start, stop the server and delete the journal file:

# Stop the orderbook server first, then:

# On Linux/Mac
rm -rf data/journal.log

# On Windows
Remove-Item -Recurse -Force data\journal.log

# Restart the server
cargo run --bin server

The data/ directory contains:

  • journal.log - All events (orders, markets, matches)

Deleting this file means the engine starts with zero state on next boot.

Option 3: Use a Fresh Data Directory​

Start the server with a different data directory:

# Linux/Mac
DATA_DIR=/tmp/fresh_orderbook cargo run --bin server

# Or simply use a timestamped directory for testing
mkdir -p data_test_$(date +%s)

What Gets Cleared​

ComponentAfter Reset
All marketsRemoved
All resting ordersRemoved
Match historyRemoved
Journal fileTruncated to 0 bytes
Sequence numbersReset to 0

Known Issues​

This section documents known limitations and issues that will be addressed in future updates.

Monitor Dashboard: In-Memory Data Only​

Issue: The Monitor Dashboard (/monitor.html) only displays events that occur while it is open. Historical data from before the dashboard was opened is not available.

Details:

  • The monitor stores all event data in browser memory only
  • Refreshing the page or opening a new tab clears all accumulated data
  • Events that occurred before connecting to the WebSocket are not visible
  • Large volumes of events may consume significant browser memory over time

Workaround:

  • Open the monitor before starting stress tests or important operations
  • Use the Export JSON/CSV feature to save data before refreshing
  • For production monitoring, integrate with external logging/monitoring systems

Status: This limitation will be addressed in a future update with:

  • Server-side event history buffering
  • Persistent storage for event logs
  • Historical event replay on dashboard connection

See Also​