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