Forsyt Orderbook API Reference
This document provides a detailed guide to the gRPC API for the Forsyt Universal Orderbook. It covers market creation, lifecycle management, order submission, and real-time data streaming.
See also: INTEGRATION_GUIDE.md for TypeScript code examples and configuration options.
Overview
The Orderbook exposes a gRPC service OrderBookService running by default on port 50051.
Proto File Location
proto/orderbook.proto
Proto Definition
The Service uses the orderbook.v1 package.
Quick Connection Example (TypeScript)
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()
);
Services
1. Market Management
CreateMarket
Initializes a new market in the engine. Markets start in the CREATED state and must be explicitly opened.
Request: CreateMarketRequest
market_id(string): Unique identifier for the market.outcomes(repeated string): List of outcome IDs (e.g., ["Home", "Draw", "Away"]).
Example:
client.CreateMarket({
market_id: 'match_liverpool_arsenal',
outcomes: ['Liverpool', 'Draw', 'Arsenal']
}, (err, response) => {
console.log('Market created:', response.status); // "CREATED"
});
OpenMarket
Transitions a market from CREATED (or SUSPENDED) to OPEN. Bets are only accepted when OPEN.
Request: MarketLifecycleRequest
market_id(string): Target market.reason(string): Optional audit reason.
Example:
client.OpenMarket({
market_id: 'match_liverpool_arsenal',
reason: 'Match kicked off'
}, (err, response) => {
console.log('Market status:', response.status); // "OPEN"
});
CloseMarket / SuspendMarket
SuspendMarket: Pauses betting (e.g., during dangerous play).CloseMarket: Permanently closes the market for settlement.
2. Order Management
SubmitOrder
Places a new bet into the order book.
Request: SubmitOrderRequest
market_id(string): Target market.outcome_id(string): Target outcome (must exist in market).side(string):"BACK"or"LAY".price(double): The decimal odds (e.g., 2.50).price_type(enum): Use1for DECIMAL_ODDS (recommended).quantity(uint64): Stake in smallest currency unit.order_type(string):"LIMIT"or"MARKET".client_order_id(string): Optional idempotency key.user_id(string): Optional User ID for Self-Trade Prevention (STP).
Example - BACK Bet (LIMIT):
// BACK Liverpool to win at 2.50 odds, £100 stake
client.SubmitOrder({
market_id: 'match_liverpool_arsenal',
outcome_id: 'Liverpool',
side: 'BACK',
price: 2.50,
price_type: 1, // DECIMAL_ODDS
quantity: 10000, // £100.00 in pence
order_type: 'LIMIT',
user_id: 'user_123'
}, (err, response) => {
console.log('Order ID:', response.order_id);
});
Example - LAY Bet (LIMIT):
// LAY Liverpool (bet against them winning) at 2.50 odds
client.SubmitOrder({
market_id: 'match_liverpool_arsenal',
outcome_id: 'Liverpool',
side: 'LAY',
price: 2.50,
price_type: 1,
quantity: 10000,
order_type: 'LIMIT',
user_id: 'user_456'
}, callback);
Example - MARKET Order:
// BACK at best available odds
client.SubmitOrder({
market_id: 'match_liverpool_arsenal',
outcome_id: 'Liverpool',
side: 'BACK',
quantity: 10000,
order_type: 'MARKET',
user_id: 'user_123'
}, callback);
CancelOrder
Cancels an active resting order.
Request: CancelOrderRequest
market_id(string)order_id(uint64): The internal ID returned bySubmitOrder.
Example:
client.CancelOrder({
market_id: 'match_liverpool_arsenal',
order_id: 12345
}, (err, response) => {
console.log('Cancelled:', response.status);
});
3. Data Retrieval
GetOrderBook
Snapshot of current BACK and LAY orders.
Request: GetOrderBookRequest
market_id(string)outcome_id(string)depth(uint32): Max levels to return (0 = all).
Response:
bids: BACK orders waiting to be matched (best odds first).asks: LAY orders waiting to be matched (best odds first).overround: Sum of implied probabilities for all outcomes.
Example:
client.GetOrderBook({
market_id: 'match_liverpool_arsenal',
outcome_id: 'Liverpool',
depth: 5 // Top 5 price levels
}, (err, response) => {
console.log('Best BACK available:', response.asks[0]?.price);
console.log('Best LAY available:', response.bids[0]?.price);
});
SubscribeMarket
Real-time stream of match events (trades, order updates, book changes).
Request: SubscribeMarketRequest
market_id(string)
Example:
const stream = client.SubscribeMarket({ market_id: 'match_liverpool_arsenal' });
stream.on('data', (update: any) => {
if (update.match_event) {
console.log(`Trade: ${update.match_event.quantity} @ ${update.match_event.price}`);
}
});
Stream Events:
match_event: A trade occurred (includes maker_order_id, taker_order_id, price, quantity)order_update: Order status changedbook_update: Order book levels changed
Feature Details
Self-Trade Prevention (STP)
When a user_id is provided, the engine prevents users from matching against their own orders. If a BACK order would match a LAY order from the same user, the engine skips that match and looks for the next available counterparty.
Synthetic Matching
The engine supports synthetic matching across related outcomes in 1X2 and multi-runner markets:
- Implied Liquidity: LAY bets on "Draw" + "Away" create implied liquidity for BACK bets on "Home".
- Cross-Market: A BACK bet on one outcome is mathematically equivalent to LAY bets on all other outcomes.
Asian Handicap Support
The engine supports all Asian Handicap line types:
| Line Type | Format | Behavior |
|---|---|---|
| Half-goal | Home_-0.5, Home_-1.5 | Binary outcome (WIN/LOSE) |
| Whole-goal | Home_-1.0, Home_-2.0 | Ternary outcome (WIN/LOSE/PUSH) |
| Quarter-goal | Home_-0.25, Home_-0.75 | Automatically splits into two legs |
Quarter-goal lines are automatically split by the engine:
Home_-0.25→ 50% onHome_0.0+ 50% onHome_-0.5Home_-0.75→ 50% onHome_-0.5+ 50% onHome_-1.0
Journaling & Recovery
All bets are journaled to data/journal.log before acknowledgment. On restart, the engine replays this log to restore exact state.
Resetting the Orderbook
To clear all state and start fresh, delete the journal file and restart:
# Stop server, then:
rm data/journal.log # Linux/Mac
# or
Remove-Item data\journal.log # Windows
# Restart server
cargo run --bin server
Error Codes
| Code | Name | Description |
|---|---|---|
INVALID_ARGUMENT | Validation Error | Price/quantity/side invalid |
NOT_FOUND | Market/Order Not Found | Resource doesn't exist |
FAILED_PRECONDITION | Market Not Open | Market state doesn't allow operation |
INTERNAL | Engine Error | Internal engine failure |
Rejection Reasons (in SubmitOrderResponse)
| Reason | Description |
|---|---|
MARKET_NOT_OPEN | Market is not in OPEN state |
INVALID_OUTCOME | Outcome ID doesn't exist in market |
INVALID_PRICE | Price is zero, negative, or off tick grid |
SELF_TRADE | Would match against same user_id |
DUPLICATE_CLIENT_ID | client_order_id already used |
Price Type Reference
Recommended: Use price_type: 1 (DECIMAL_ODDS) for all bets.
| Enum Value | Name | Description | Example |
|---|---|---|---|
| 1 | DECIMAL_ODDS | European decimal odds (recommended) | 2.50 = 40% implied prob |
| 0 | BASIS_POINTS | Raw internal format | 400000 = 40% |
Decimal Odds Conversion
// Decimal odds to probability
const probability = 1 / decimalOdds;
// Example: 2.50 → 0.40 (40%)
// Probability to decimal odds
const decimalOdds = 1 / probability;
// Example: 0.40 → 2.50
// Decimal odds to BasisPoints (internal format)
const basisPoints = Math.round((1 / decimalOdds) * 1_000_000);
// Example: 2.50 → 400,000 BP
Complete Service Definition
service OrderBookService {
// Market Lifecycle
rpc CreateMarket(CreateMarketRequest) returns (CreateMarketResponse);
rpc OpenMarket(MarketLifecycleRequest) returns (MarketLifecycleResponse);
rpc SuspendMarket(MarketLifecycleRequest) returns (MarketLifecycleResponse);
rpc CloseMarket(MarketLifecycleRequest) returns (MarketLifecycleResponse);
// Order Management
rpc SubmitOrder(SubmitOrderRequest) returns (SubmitOrderResponse);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
// Queries
rpc GetOrderBook(GetOrderBookRequest) returns (GetOrderBookResponse);
// Streaming
rpc SubscribeMarket(SubscribeMarketRequest) returns (stream MarketUpdate);
}
Message Reference
CreateMarketRequest
message CreateMarketRequest {
string market_id = 1; // Unique market identifier
repeated string outcomes = 2; // ["Home", "Draw", "Away"]
}
SubmitOrderRequest
message SubmitOrderRequest {
string market_id = 1;
string outcome_id = 2;
string side = 3; // "BACK" or "LAY"
double price = 4; // Decimal odds (e.g., 2.50)
PriceType price_type = 5; // Use 1 for DECIMAL_ODDS
uint64 quantity = 6; // Stake in smallest currency unit
string order_type = 7; // "LIMIT" or "MARKET"
string client_order_id = 8; // Optional idempotency key
string user_id = 9; // Optional for STP
}
GetOrderBookResponse
message GetOrderBookResponse {
string market_id = 1;
string outcome_id = 2;
repeated Level bids = 3; // BACK orders (sorted by best odds first)
repeated Level asks = 4; // LAY orders (sorted by best odds first)
double overround = 5; // Sum of implied probabilities
}
message Level {
uint64 price = 1; // BasisPoints (internal format)
uint64 quantity = 2; // Total stake at this level
int32 order_count = 3; // Number of orders at this level
}
MarketUpdate (Streaming)
message MarketUpdate {
oneof update {
MatchEvent match_event = 1;
OrderUpdateEvent order_update = 2;
BookUpdateEvent book_update = 3;
}
}
message MatchEvent {
uint64 sequence = 1; // Event sequence number
uint64 maker_order_id = 2; // Resting order that got matched
uint64 taker_order_id = 3; // Incoming order that matched
uint64 price = 4; // Match price in BasisPoints
uint64 quantity = 5; // Matched stake
uint64 timestamp = 6; // Unix timestamp in nanoseconds
}
WebSocket Event API
In addition to the gRPC API, the orderbook provides real-time event streaming via WebSocket at ws://localhost:3000/ws.
Event Types Summary
| Event Type | Description |
|---|---|
order_submit | New order submitted to the orderbook |
match | Two orders matched (trade executed) |
order_filled | Order completely filled (remaining = 0) |
order_expired | Order expired due to TTL |
order_cancelled | Order cancelled by user or system |
market_created | New market created |
Event Schemas
All events include a type field for discriminating the event type.
order_submit
{
"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
}
match
{
"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
}
order_filled
{
"type": "order_filled",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
order_expired
{
"type": "order_expired",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
order_cancelled
{
"type": "order_cancelled",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
See also: INTEGRATION_GUIDE.md for complete TypeScript integration examples.
Rate Limits & Performance
| Metric | Value |
|---|---|
| Sustained throughput | 10,000+ orders/sec |
| P99 latency | < 50ms |
| Max concurrent streams | 1,000 |
| Channel buffer size | 4,096 messages |
Known Issues
Monitor Dashboard: In-Memory Data Only
The Monitor Dashboard (/monitor.html) only displays events that occur while it is open. Historical data is not persisted and is lost on page refresh. This will be addressed in a future update with server-side event buffering and historical replay.
See INTEGRATION_GUIDE.md#known-issues for details and workarounds.
See Also
- INTEGRATION_GUIDE.md - Complete integration examples
- OrderBook_Universal_Architecture.md - Architecture deep dive
- Testing_Architecture.md - Testing strategies