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