Skip to main content

bOS v3.0 — Scoring Engine Technical Specification

Real-Time Fantasy Points Calculation System

Version: 3.0 | Last Updated: January 2026 | Status: Design Phase Audience: Engineering Team


Table of Contents

  1. Overview
  2. Architecture
  3. Event Processing Pipeline
  4. Scoring Rules Engine
  5. Sport-Specific Configurations
  6. Real-Time Leaderboard
  7. Props Resolution
  8. Trading Price Updates
  9. Settlement Engine
  10. Performance Requirements

1. Overview

1.1 Purpose

The Scoring Engine is the real-time brain of the bOS fantasy platform. It:

  1. Ingests live match events from data providers
  2. Calculates fantasy points for all athletes in real-time
  3. Updates leaderboards across all contests
  4. Resolves player props as outcomes are determined
  5. Adjusts trading market prices based on events
  6. Triggers settlements when matches complete

1.2 Design Principles

PrincipleDescription
Event-DrivenAll updates triggered by match events, not polling
IdempotentSame event processed multiple times = same result
AuditableEvery point calculation traceable to source event
ConfigurableScoring rules defined in config, not code
ScalableHandle 100+ concurrent matches, 1M+ entries

1.3 System Context


2. Architecture

2.1 Component Overview

2.2 Technology Stack

ComponentTechnologyJustification
Event IngestionNode.js + WebSocketReal-time, async I/O
Message QueueApache KafkaOrdered, durable, replayable

3. Event Processing Pipeline

3.1 Event Types

Cricket T20 Events

Event TypeDescriptionFantasy ImpactProps ImpactTrading Impact
ball.completedSingle deliveryPoints for runs/wicketCheck thresholdsPrice adjustment
over.completedOver finishedMaiden bonusEconomy checkMinor adjustment
innings.completedInnings endsBatting bonusesResolve batting propsMajor adjustment
match.completedMatch endsFinal settlementAll props resolvedAll markets settle
player.milestone50, 100, hat-trickBonus pointsMilestone propsSignificant move
player.outBatsman dismissedDuck penaltyBatting props lockPrice adjustment

Event Schema

interface MatchEvent {
eventId: string; // Unique event identifier
matchId: string; // Match reference
eventType: EventType; // Type of event
timestamp: number; // Unix timestamp (ms)
sequence: number; // Ordered sequence number

// Event-specific payload
payload: BallEvent | OverEvent | InningsEvent | MatchEvent;

// Metadata
source: string; // Data provider
version: string; // Schema version
receivedAt: number; // When we received it
}

interface BallEvent {
innings: number;
over: number;
ball: number;

batter: {
playerId: string;
name: string;
runs: number;
ballsFaced: number;
fours: number;
sixes: number;
strikeRate: number;
};

bowler: {
playerId: string;
name: string;
runsConceded: number;
wickets: number;
overs: number;
economy: number;
maidens: number;
};

outcome: {
runs: number;
extras: number;
extraType?: 'wide' | 'noball' | 'bye' | 'legbye';
isWicket: boolean;
wicketType?: 'bowled' | 'caught' | 'lbw' | 'runout' | 'stumped';
fielder?: string;
};

matchState: {
score: number;
wickets: number;
overs: number;
runRate: number;
target?: number;
};
}

3.2 Event Validation

3.3 Deduplication Strategy

Events are deduplicated using a composite key:

Key = {matchId}:{eventType}:{innings}:{over}:{ball}:{sequence}

Redis SET with 24-hour TTL stores processed event keys.


4. Scoring Rules Engine

4.1 Rule Configuration Schema

# scoring_rules/cricket_t20.yaml
sport: cricket
format: t20
version: "2.0"

batting:
run:
points: 1
description: "Per run scored"

boundary_four:
points: 1
bonus: true
description: "Bonus for hitting a four"

boundary_six:
points: 2
bonus: true
description: "Bonus for hitting a six"

milestone_50:
points: 8
trigger:
stat: runs
threshold: 50
comparison: gte
description: "Half-century bonus"

milestone_100:
points: 16
trigger:
stat: runs
threshold: 100
comparison: gte
description: "Century bonus"

strike_rate_bonus:
conditions:
- min_balls: 10
ranges:
- min: 170
points: 6
- min: 150
max: 169.99
points: 4
- min: 130
max: 149.99
points: 2

strike_rate_penalty:
conditions:
- min_balls: 10
ranges:
- max: 50
points: -6
- min: 50.01
max: 60
points: -4
- min: 60.01
max: 70
points: -2

duck:
points: -2
trigger:
stat: runs
threshold: 0
comparison: eq
additional:
- stat: balls_faced
threshold: 1
comparison: gte
- stat: is_out
value: true

bowling:
wicket:
points: 25
description: "Per wicket taken"
exclusions:
- runout # Run outs don't count for bowler

lbw_bowled_bonus:
points: 8
trigger:
wicket_type:
- lbw
- bowled
description: "Bonus for LBW or bowled"

maiden:
points: 12
description: "Maiden over"

milestone_3_wickets:
points: 4
trigger:
stat: wickets
threshold: 3
comparison: gte

milestone_4_wickets:
points: 8
trigger:
stat: wickets
threshold: 4
comparison: gte

milestone_5_wickets:
points: 16
trigger:
stat: wickets
threshold: 5
comparison: gte

economy_bonus:
conditions:
- min_overs: 2
ranges:
- max: 5
points: 6
- min: 5.01
max: 6
points: 4
- min: 6.01
max: 7
points: 2

economy_penalty:
conditions:
- min_overs: 2
ranges:
- min: 12
points: -6
- min: 11
max: 11.99
points: -4
- min: 10
max: 10.99
points: -2

fielding:
catch:
points: 8

stumping:
points: 12

runout_direct:
points: 12

runout_indirect:
points: 6

multipliers:
captain:
factor: 2.0
description: "Captain gets 2x points"

vice_captain:
factor: 1.5
description: "Vice-captain gets 1.5x points"

4.2 Rules Engine Implementation

// Simplified Rust implementation
pub struct ScoringEngine {
rules: ScoringRules,
cache: RedisClient,
}

impl ScoringEngine {
pub async fn process_ball_event(&self, event: &BallEvent) -> Result<Vec<PointUpdate>> {
let mut updates = Vec::new();

// Calculate batter points
let batter_points = self.calculate_batter_points(event)?;
updates.push(PointUpdate {
player_id: event.batter.player_id.clone(),
points: batter_points,
breakdown: self.get_breakdown(&event.batter),
});

// Calculate bowler points
let bowler_points = self.calculate_bowler_points(event)?;
updates.push(PointUpdate {
player_id: event.bowler.player_id.clone(),
points: bowler_points,
breakdown: self.get_breakdown(&event.bowler),
});

// Calculate fielder points if applicable
if let Some(fielder) = &event.outcome.fielder {
let fielder_points = self.calculate_fielder_points(event)?;
updates.push(PointUpdate {
player_id: fielder.clone(),
points: fielder_points,
breakdown: vec![("fielding", fielder_points)],
});
}

Ok(updates)
}

fn calculate_batter_points(&self, event: &BallEvent) -> Result<f64> {
let mut points = 0.0;
let batter = &event.batter;

// Base runs
points += batter.runs as f64 * self.rules.batting.run.points;

// Boundary bonuses
points += batter.fours as f64 * self.rules.batting.boundary_four.points;
points += batter.sixes as f64 * self.rules.batting.boundary_six.points;

// Milestone bonuses (check if just crossed)
if self.just_crossed_milestone(batter, 50) {
points += self.rules.batting.milestone_50.points;
}
if self.just_crossed_milestone(batter, 100) {
points += self.rules.batting.milestone_100.points;
}

// Strike rate bonus/penalty (only at end of innings)
// Handled separately in innings_completed event

Ok(points)
}
}

5. Sport-Specific Configurations

5.1 Supported Sports

SportFormatStatusPriority
CricketT20, ODI, TestActiveP0
Football90 minPlannedP1
KabaddiPro KabaddiPlannedP2
BasketballNBA, WNBAFutureP3

5.2 Cricket Formats Comparison

Scoring ElementT20ODITest
Run+1+1+1
Boundary (4)+1 bonus+1 bonus+1 bonus
Six+2 bonus+2 bonus+2 bonus
Half-century+8+4+4
Century+16+8+8
Wicket+25+25+25
Maiden+12+4+2
SR Bonus (min balls)1020N/A
Economy Bonus (min overs)2510

5.3 Adding New Sports

To add a new sport:

  1. Create scoring rules YAML in config/scoring_rules/{sport}_{format}.yaml
  2. Implement event adapter for data provider
  3. Add event type mappings
  4. Create sport-specific validation rules
  5. Add to supported sports registry

6. Real-Time Leaderboard

6.1 Leaderboard Architecture

6.2 Redis Data Structures

# Player scores for a match
HSET match:{matchId}:players {playerId} {totalPoints}

# Entry scores for a contest
HSET contest:{contestId}:entries {entryId} {totalPoints}

# Leaderboard sorted set
ZADD contest:{contestId}:leaderboard {score} {entryId}

# Entry details (for display)
HSET entry:{entryId} userId {userId} teamName {name} captain {captainId} ...

# Get top 100
ZREVRANGE contest:{contestId}:leaderboard 0 99 WITHSCORES

# Get user's rank
ZREVRANK contest:{contestId}:leaderboard {entryId}

6.3 Update Flow

6.4 Leaderboard API Response

interface LeaderboardResponse {
contestId: string;
matchId: string;
totalEntries: number;
lastUpdated: number;

entries: LeaderboardEntry[];

userEntry?: {
rank: number;
entry: LeaderboardEntry;
prizeZone: boolean;
pointsToNextPrize: number;
};
}

interface LeaderboardEntry {
rank: number;
entryId: string;
userId: string;
username: string;
teamName: string;
points: number;
captain: {
playerId: string;
name: string;
points: number;
};
viceCaptain: {
playerId: string;
name: string;
points: number;
};
}

7. Props Resolution

7.1 Prop Types and Resolution

Prop TypeResolution TriggerExample
RunsPlayer out or innings endRohit 32.5+ runs
WicketsMatch endBumrah 1.5+ wickets
BoundariesPlayer out or innings endKohli 3.5+ fours
Strike RatePlayer out (min balls)Hardik 140+ SR
EconomyMatch end (min overs)Chahar under 8.0
Team TotalInnings endMI 175+ runs

7.2 Resolution Engine

interface PropLine {
propId: string;
matchId: string;
playerId?: string;
teamId?: string;

statType: 'runs' | 'wickets' | 'boundaries' | 'strike_rate' | 'economy' | 'team_total';
line: number;

status: 'open' | 'locked' | 'resolved';
outcome?: 'over' | 'under' | 'push';

resolutionTrigger: 'player_out' | 'innings_end' | 'match_end';
resolvedAt?: number;
finalValue?: number;
}

class PropsResolver {
async onPlayerOut(event: PlayerOutEvent): Promise<void> {
// Find all props for this player that resolve on out
const props = await this.getPropsForPlayer(event.playerId, 'player_out');

for (const prop of props) {
const finalValue = await this.getFinalStat(event.playerId, prop.statType);
const outcome = this.determineOutcome(finalValue, prop.line);

await this.resolveProp(prop.propId, outcome, finalValue);
await this.settleSlips(prop.propId, outcome);
}
}

determineOutcome(value: number, line: number): 'over' | 'under' | 'push' {
if (value > line) return 'over';
if (value < line) return 'under';
return 'push'; // Exact match = refund
}
}

7.3 Slip Settlement


8. Trading Price Updates

8.1 Price Update Triggers

EventPrice ImpactExample
Run scoredMinor adjustment+0.5% for batting milestone market
WicketModerate adjustment-10% for batting milestone market
Milestone reachedMarket settles50+ runs market settles YES
Match state changeSignificant adjustmentTeam needs 50 off 30 balls

8.2 Probability Model

For player milestone markets (e.g., "Rohit 50+ runs"):

def calculate_probability(current_runs, balls_faced, balls_remaining, historical_sr):
"""
Calculate probability of reaching milestone based on current state.
"""
runs_needed = 50 - current_runs

if runs_needed <= 0:
return 1.0 # Already achieved

if balls_remaining <= 0:
return 0.0 # No balls left

# Expected runs based on historical strike rate
expected_runs = (balls_remaining * historical_sr) / 100

# Probability using normal distribution
# Adjust for match situation, pressure, etc.
std_dev = expected_runs * 0.4 # Volatility factor

probability = 1 - norm.cdf(runs_needed, expected_runs, std_dev)

return max(0.01, min(0.99, probability)) # Clamp to 1-99%

8.3 AMM Price Update

impl TradingEngine {
pub async fn on_ball_event(&self, event: &BallEvent) -> Result<()> {
// Find all markets affected by this event
let markets = self.get_affected_markets(event).await?;

for market in markets {
// Calculate new probability
let new_prob = self.calculate_probability(&market, event)?;

// Adjust AMM reserves to reflect new probability
self.adjust_reserves(&market, new_prob).await?;

// Publish price update
self.publish_price_update(&market).await?;
}

Ok(())
}

fn adjust_reserves(&self, market: &Market, target_prob: f64) -> Result<()> {
// Current reserves
let (yes_reserve, no_reserve) = market.reserves;
let total = yes_reserve + no_reserve;

// New reserves to achieve target probability
// Price_YES = no_reserve / total
// target_prob = new_no / total
// new_no = target_prob * total
let new_no = target_prob * total;
let new_yes = total - new_no;

market.reserves = (new_yes, new_no);
Ok(())
}
}

9. Settlement Engine

9.1 Settlement Triggers

ModeTriggerSettlement Time
Fantasymatch.completed event< 5 minutes
PropsIndividual stat finalizedReal-time
TradingMarket close condition metImmediate
PoolsOutcome confirmed< 5 minutes

9.2 Fantasy Settlement Flow

9.3 Prize Distribution Algorithm

interface PrizeStructure {
ranks: PrizeRank[];
totalPrizePool: number;
platformRake: number;
}

interface PrizeRank {
fromRank: number;
toRank: number;
prizePercentage?: number; // Percentage of pool
fixedPrize?: number; // Fixed amount
}

function calculatePrizes(
leaderboard: LeaderboardEntry[],
structure: PrizeStructure
): Map<string, number> {
const prizes = new Map<string, number>();
const distributablePool = structure.totalPrizePool * (1 - structure.platformRake);

for (const rank of structure.ranks) {
const entriesInRange = leaderboard.filter(
e => e.rank >= rank.fromRank && e.rank <= rank.toRank
);

if (entriesInRange.length === 0) continue;

let prizePerEntry: number;

if (rank.prizePercentage) {
const totalForRange = distributablePool * rank.prizePercentage;
prizePerEntry = totalForRange / entriesInRange.length;
} else if (rank.fixedPrize) {
prizePerEntry = rank.fixedPrize;
}

for (const entry of entriesInRange) {
prizes.set(entry.entryId, prizePerEntry);
}
}

return prizes;
}

9.4 Tie-Breaking Rules

PriorityRuleDescription
1Total PointsHigher points wins
2Captain PointsHigher captain points wins
3Vice-Captain PointsHigher VC points wins
4Entry TimeEarlier entry wins
5Split PrizeEqual split if still tied

10. Performance Requirements

10.1 Latency Targets

OperationTargetP99
Event ingestion< 50ms< 100ms
Points calculation< 10ms< 25ms
Leaderboard update< 100ms< 200ms
WebSocket push< 50ms< 100ms
End-to-end< 500ms< 1s

10.2 Throughput Requirements

MetricRequirement
Events per second1,000+
Concurrent matches100+
Entries per match100,000+
WebSocket connections500,000+
Leaderboard updates/sec10,000+

10.3 Availability

MetricTarget
Uptime99.9%
Recovery Time Objective (RTO)< 5 minutes
Recovery Point Objective (RPO)< 1 minute
Failover time< 30 seconds

10.4 Monitoring & Alerting

MetricAlert Threshold
Event processing lag> 5 seconds
Leaderboard staleness> 30 seconds
Error rate> 0.1%
CPU usage> 80%
Memory usage> 85%
Redis latency> 10ms

Document maintained by bOS Engineering Team. Last updated January 2026.