Skip to main content

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): Use 1 for 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 by SubmitOrder.

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 changed
  • book_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 TypeFormatBehavior
Half-goalHome_-0.5, Home_-1.5Binary outcome (WIN/LOSE)
Whole-goalHome_-1.0, Home_-2.0Ternary outcome (WIN/LOSE/PUSH)
Quarter-goalHome_-0.25, Home_-0.75Automatically splits into two legs

Quarter-goal lines are automatically split by the engine:

  • Home_-0.25 → 50% on Home_0.0 + 50% on Home_-0.5
  • Home_-0.75 → 50% on Home_-0.5 + 50% on Home_-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​

CodeNameDescription
INVALID_ARGUMENTValidation ErrorPrice/quantity/side invalid
NOT_FOUNDMarket/Order Not FoundResource doesn't exist
FAILED_PRECONDITIONMarket Not OpenMarket state doesn't allow operation
INTERNALEngine ErrorInternal engine failure

Rejection Reasons (in SubmitOrderResponse)​

ReasonDescription
MARKET_NOT_OPENMarket is not in OPEN state
INVALID_OUTCOMEOutcome ID doesn't exist in market
INVALID_PRICEPrice is zero, negative, or off tick grid
SELF_TRADEWould match against same user_id
DUPLICATE_CLIENT_IDclient_order_id already used

Price Type Reference​

Recommended: Use price_type: 1 (DECIMAL_ODDS) for all bets.

Enum ValueNameDescriptionExample
1DECIMAL_ODDSEuropean decimal odds (recommended)2.50 = 40% implied prob
0BASIS_POINTSRaw internal format400000 = 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 TypeDescription
order_submitNew order submitted to the orderbook
matchTwo orders matched (trade executed)
order_filledOrder completely filled (remaining = 0)
order_expiredOrder expired due to TTL
order_cancelledOrder cancelled by user or system
market_createdNew 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​

MetricValue
Sustained throughput10,000+ orders/sec
P99 latency< 50ms
Max concurrent streams1,000
Channel buffer size4,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​