Spread Engine
The Spread Engine calculates the casino's edge on every prediction market trade. It takes the mid price from Polymarket's order book and applies a configurable spread to produce ask (buy) and bid (sell) prices. The spread is the primary source of casino revenue - it is earned on every trade regardless of the market outcome.
src/lib/engines/spread-engine.ts. The spread engine is a pure function with no database dependencies. It receives market data as inputs and returns spread-adjusted prices.What is the Spread?
In prediction markets, shares trade at a price between 0c and 100c. The mid priceis the true market price sourced from Polymarket. The spread is a markup/markdown applied symmetrically around the mid price:
- Ask price (for buyers) = mid + half spread. The player pays more to buy.
- Bid price (for sellers) = mid - half spread. The player receives less when selling.
Mid price: 50.00c
Spread: 4.00% (2.00c in this case)
Ask price: 51.00c ← player pays this to BUY
Bid price: 49.00c ← player receives this to SELL
Casino captures: 2.00c per share on every round-trip trade.The spread is the casino's edge. Unlike a sportsbook where the house takes a cut only on losing bets, the spread is earned on every trade - buys and sells alike. The casino profits even when the player wins, as long as the total spread collected exceeds the net payouts.
Base Spread Configuration
The spread engine uses three configurable parameters that define the bounds:
| Parameter | Default | Description |
|---|---|---|
defaultPct | 4 | Base spread percentage applied to every trade. A 50c mid price produces a 2c spread (51c ask, 49c bid). |
minPct | 1 | Minimum spread percentage. Even under favorable conditions, the spread never goes below this. |
maxPct | 15 | Maximum spread percentage. Caps the spread during extreme widening conditions. |
interface SpreadConfig {
defaultPct: number; // e.g. 4 = 4%
minPct: number; // floor
maxPct: number; // ceiling
}
const DEFAULT_CONFIG: SpreadConfig = {
defaultPct: 4,
minPct: 1,
maxPct: 15,
};Dynamic Spread Widening
The spread is not static. It widens dynamically based on three market conditions to protect the casino from adverse scenarios:
Factor 1: Extreme Prices (Near 0c or 100c)
Markets near certainty (close to 100c) or near impossibility (close to 0c) have asymmetric risk. The spread widens up to 2x at the extreme edges to compensate.
| Mid Price | Distance from Edge | Multiplier | Effective Spread (4% base) |
|---|---|---|---|
| 50c | 50c | 1.00x | 4.00% |
| 80c | 20c | 1.00x | 4.00% |
| 90c | 10c | 1.00x | 4.00% |
| 92c | 8c | 1.20x | 4.80% |
| 95c | 5c | 1.50x | 6.00% |
| 97c | 3c | 1.70x | 6.80% |
| 99c | 1c | 1.90x | 7.60% |
| 5c | 5c | 1.50x | 6.00% |
| 2c | 2c | 1.80x | 7.20% |
// Extreme price widening formula
const distFromEdge = Math.min(midPrice, 100 - midPrice);
if (distFromEdge < 10) {
// Multiplier ranges from 1.0 (at 10c from edge) to 2.0 (at 0c from edge)
spreadPct *= 1 + (10 - distFromEdge) / 10;
}
// Examples:
// midPrice = 95 → distFromEdge = 5 → multiplier = 1 + 5/10 = 1.5
// midPrice = 99 → distFromEdge = 1 → multiplier = 1 + 9/10 = 1.9
// midPrice = 50 → distFromEdge = 50 → no widening (>= 10)Factor 2: Low Liquidity (Below $50,000)
Markets with low trading volume on Polymarket have less reliable mid prices. The spread widens inversely proportional to liquidity, up to 2xfor very illiquid markets.
| Liquidity | Liquidity Factor | Effective Spread (4% base) |
|---|---|---|
| $100,000+ | 1.00 (no widening) | 4.00% |
| $50,000 | 1.00 (threshold) | 4.00% |
| $40,000 | 0.80 → 5.00% | 5.00% |
| $25,000 | 0.50 → 8.00% | 8.00% |
| $10,000 | 0.50 (floor) → 8.00% | 8.00% |
// Low liquidity widening formula
if (liquidity != null && liquidity < 50000) {
const liquidityFactor = Math.max(0.5, liquidity / 50000);
spreadPct /= liquidityFactor;
// At $25k liquidity: spreadPct = 4 / 0.5 = 8%
// At $40k liquidity: spreadPct = 4 / 0.8 = 5%
}Factor 3: Exposure Imbalance
When the casino has significantly more exposure on one side (e.g., too many YES shares outstanding), the spread is skewed to discourage further trading in that direction and encourage the opposite side.
// Exposure skew formula
let askSkew = 0; // additional cents on ask price (makes buys more expensive)
let bidSkew = 0; // additional cents on bid price (makes sells cheaper)
if (Math.abs(exposureImbalance) > 100) {
const skewBps = Math.min(200, Math.abs(exposureImbalance) / 100);
// Maximum skew: 2 cents
if (exposureImbalance > 0) {
// Casino has too much YES exposure
// → make YES buys MORE expensive (wider ask)
askSkew = skewBps / 100;
} else {
// Casino has too much NO exposure
// → make sells CHEAPER (wider bid)
bidSkew = skewBps / 100;
}
}Complete Spread Formula
Here is the full spread calculation pipeline from mid price to ask/bid prices:
function calculateSpread(midPrice, config, options?) {
// Start with base spread (or custom override)
let spreadPct = options?.customSpreadPct ?? config.defaultPct; // 4%
// Add per-user sharp adjustment (wider for detected sharps)
if (options?.sharpAdjustment) {
spreadPct += options.sharpAdjustment; // +1 to +3 extra %
}
// Factor 1: Widen for extreme prices (near 0 or 100)
const distFromEdge = Math.min(midPrice, 100 - midPrice);
if (distFromEdge < 10) {
spreadPct *= 1 + (10 - distFromEdge) / 10; // up to 2x
}
// Factor 2: Widen for low liquidity
if (liquidity < 50000) {
const liquidityFactor = Math.max(0.5, liquidity / 50000);
spreadPct /= liquidityFactor; // up to 2x
}
// Factor 3: Calculate exposure skew (asymmetric ask/bid adjustment)
let askSkew = 0, bidSkew = 0;
if (Math.abs(exposureImbalance) > 100) {
const skewBps = Math.min(200, Math.abs(exposureImbalance) / 100);
if (exposureImbalance > 0) askSkew = skewBps / 100;
else bidSkew = skewBps / 100;
}
// Clamp spread to min/max bounds
spreadPct = Math.max(config.minPct, Math.min(config.maxPct, spreadPct));
// Convert percentage to cents
const spreadCents = (midPrice * spreadPct) / 100;
const halfSpread = spreadCents / 2;
// Calculate final prices
const askPrice = Math.min(99, Math.round((midPrice + halfSpread + askSkew) * 100) / 100);
const bidPrice = Math.max(1, Math.round((midPrice - halfSpread - bidSkew) * 100) / 100);
return { midPrice, askPrice, bidPrice, spreadPct, spreadCents: askPrice - bidPrice };
}getExecutionPrice Helper
The getExecutionPrice function is the primary entry point used by the trade executor. It wraps calculateSpread and returns just the relevant price.
function getExecutionPrice(midPrice, side, config?, options?) {
const spread = calculateSpread(midPrice, config, options);
return side === "buy" ? spread.askPrice : spread.bidPrice;
}
// Buy: player pays the ask price (higher than mid)
getExecutionPrice(50, "buy") // → 51 (mid + spread)
// Sell: player receives the bid price (lower than mid)
getExecutionPrice(50, "sell") // → 49 (mid - spread)Market-Specific Spread Overrides
Individual markets can have custom spread percentages configured via the operator dashboard or God Mode admin panel. This overrides the defaultPct for that specific market while still applying all dynamic widening factors.
// Check for market-specific override
const spreadOverride = await getMarketSpreadOverride(sb, marketId);
const effectiveConfig = spreadOverride
? { ...DEFAULT_SPREAD, defaultPct: spreadOverride }
: DEFAULT_SPREAD;
// If market has a 6% override:
// defaultPct becomes 6 instead of 4
// Dynamic widening still applies on top of 6%Sharp Bettor Spread Adjustment
The User Scoring Engine assigns each player a composite score based on their trading patterns. Players identified as "sharp" (consistently profitable, likely using bots or advanced strategies) receive a wider spread as an additional risk management layer.
| User Tier | Composite Score | Additional Spread |
|---|---|---|
new | Any | +0% |
regular | < 60 | +0% |
regular | 60–80 | +1% |
regular | > 80 | +2% |
vip | Any | +0% |
restricted | Any | +3% |
function getSharpSpreadAdjustment(tier, userScore?) {
// Restricted users always get wider spreads
if (tier === "restricted") return 3;
// High-scoring users (sharps) get moderately wider spreads
if (userScore != null && userScore > 80) return 2;
if (userScore != null && userScore > 60) return 1;
return 0;
}restricted tier).Example Calculations
Example 1: Normal Market (50c mid, standard user)
| Step | Value |
|---|---|
| Mid price | 50.00c |
| Base spread | 4.00% |
| Sharp adjustment | +0% |
| Extreme price widening | None (50c from edge) |
| Liquidity widening | None ($100k+) |
| Exposure skew | None (balanced) |
| Final spread | 4.00% |
| Spread in cents | 2.00c |
| Ask price | 51.00c |
| Bid price | 49.00c |
A player buying $100 of YES at 51c receives 196.08 shares. If the market resolves YES, they receive $196.08. Net profit: $96.08. If they immediately sell at 49c, they receive $96.08. Net loss: $3.92 (the spread).
Example 2: Extreme Price Market (95c mid, standard user)
| Step | Value |
|---|---|
| Mid price | 95.00c |
| Base spread | 4.00% |
| Sharp adjustment | +0% |
| Extreme price widening | distFromEdge=5 → 1.5x multiplier |
| Effective spread after widening | 6.00% |
| Liquidity widening | None ($100k+) |
| Exposure skew | None |
| Final spread | 6.00% |
| Spread in cents | 5.70c |
| Ask price | 97.85c (rounded) |
| Bid price | 92.15c (rounded) |
Example 3: Low Liquidity Market (50c mid, $20k liquidity)
| Step | Value |
|---|---|
| Mid price | 50.00c |
| Base spread | 4.00% |
| Extreme price widening | None (50c from edge) |
| Liquidity widening | factor = max(0.5, 20000/50000) = 0.5 → 2x |
| Effective spread | 8.00% |
| Final spread | 8.00% |
| Spread in cents | 4.00c |
| Ask price | 52.00c |
| Bid price | 48.00c |
Example 4: Sharp Bettor (50c mid, restricted tier)
| Step | Value |
|---|---|
| Mid price | 50.00c |
| Base spread | 4.00% |
| Sharp adjustment | +3% (restricted tier) |
| Effective spread before widening | 7.00% |
| Extreme price widening | None |
| Liquidity widening | None |
| Final spread | 7.00% |
| Spread in cents | 3.50c |
| Ask price | 51.75c |
| Bid price | 48.25c |
Example 5: Worst Case (97c mid, $10k liquidity, restricted user)
| Step | Value |
|---|---|
| Mid price | 97.00c |
| Base spread | 4.00% |
| Sharp adjustment | +3% (restricted tier) |
| Spread before widening | 7.00% |
| Extreme price widening | distFromEdge=3 → 1.7x |
| Spread after extreme widening | 11.90% |
| Liquidity widening | factor = 0.5 → 2x |
| Spread after liquidity widening | 23.80% (exceeds max!) |
| Clamped to max | 15.00% |
| Spread in cents | 14.55c |
| Ask price | 99.00c (clamped at 99) |
| Bid price | 89.73c |
Return Value
The calculateSpread function returns a SpreadResult object with all calculated values:
interface SpreadResult {
midPrice: number; // original market price (cents 0-100)
askPrice: number; // price user pays to BUY (cents)
bidPrice: number; // price user receives to SELL (cents)
spreadPct: number; // effective spread percentage
spreadCents: number; // spread in cents (askPrice - bidPrice)
}
// Example return for a 50c market with 4% base spread:
{
midPrice: 50,
askPrice: 51,
bidPrice: 49,
spreadPct: 4,
spreadCents: 2,
}Spread as Casino Revenue
The spread engine is the casino's primary revenue mechanism. Revenue is earned in two scenarios:
Scenario 1: Round-Trip Trading
A player buys at the ask price and later sells at the bid price. The casino earns the full spread regardless of price movement.
Player buys 100 shares at 51c (ask) → pays $51.00
Player sells 100 shares at 49c (bid) → receives $49.00
Casino revenue: $51.00 - $49.00 = $2.00 (the spread)Scenario 2: Market Resolution
A player buys shares and holds until resolution. The casino earns the spread on the buy side. If the outcome is YES, shares pay $1. If NO, shares expire worthless.
Player buys 100 YES shares at 52c (ask, mid was 50c) → pays $52.00
If YES wins: player receives $100.00. Casino net: $52.00 - $100.00 = -$48.00
If NO wins: player receives $0.00. Casino net: $52.00 - $0.00 = +$52.00
Expected value for casino (at 50% probability):
0.5 * (-$48.00) + 0.5 * ($52.00) = +$2.00
The casino's expected revenue is always the spread (here $2 per 100 shares),
regardless of the outcome, as long as the mid price is accurate.All Configurable Parameters
| Parameter | Source | Default | Range | Description |
|---|---|---|---|---|
defaultPct | SpreadConfig | 4 | 1–15 | Base spread percentage for all markets. |
minPct | SpreadConfig | 1 | 0.5–5 | Minimum spread floor after all adjustments. |
maxPct | SpreadConfig | 15 | 5–25 | Maximum spread ceiling after all adjustments. |
customSpreadPct | Per-market override | - | 1–15 | Overrides defaultPct for a specific market. |
sharpAdjustment | User Scoring Engine | 0 | 0–3 | Additional spread percentage for detected sharp bettors. |
| Extreme price threshold | Hardcoded | 10c | - | Widening activates when mid price is within 10c of 0 or 100. |
| Liquidity threshold | Hardcoded | $50,000 | - | Widening activates when market liquidity drops below $50k. |
| Exposure skew threshold | Hardcoded | $100 | - | Skew activates when imbalance exceeds $100. |
| Max exposure skew | Hardcoded | 2c | - | Maximum asymmetric skew added to ask or bid. |
| Ask price ceiling | Hardcoded | 99c | - | Maximum ask price. Shares can never cost more than 99c. |
| Bid price floor | Hardcoded | 1c | - | Minimum bid price. Players always receive at least 1c per share. |
