Predictu
Trading Engine

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.

Source: 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:

ParameterDefaultDescription
defaultPct4Base spread percentage applied to every trade. A 50c mid price produces a 2c spread (51c ask, 49c bid).
minPct1Minimum spread percentage. Even under favorable conditions, the spread never goes below this.
maxPct15Maximum 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 PriceDistance from EdgeMultiplierEffective Spread (4% base)
50c50c1.00x4.00%
80c20c1.00x4.00%
90c10c1.00x4.00%
92c8c1.20x4.80%
95c5c1.50x6.00%
97c3c1.70x6.80%
99c1c1.90x7.60%
5c5c1.50x6.00%
2c2c1.80x7.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)
Why widen at extremes? At 95c, a YES share costs 95c and pays $1 if correct - only 5c upside. But if the market swings, the casino's exposure is heavily skewed. Wider spreads protect against the thin margin at these prices and discourage speculative pile-ons.

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.

LiquidityLiquidity FactorEffective Spread (4% base)
$100,000+1.00 (no widening)4.00%
$50,0001.00 (threshold)4.00%
$40,0000.80 → 5.00%5.00%
$25,0000.50 → 8.00%8.00%
$10,0000.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;
  }
}
Self-balancing mechanism: The exposure skew acts as a natural incentive to balance the book. When too many players are buying YES, the YES price goes up slightly (wider ask), making it less attractive. This is similar to how a sportsbook moves the line when one side gets too much action.

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%
Use cases for overrides: High-profile markets (elections, Super Bowl) may justify tighter spreads to attract volume. Exotic or volatile markets may need wider spreads to manage risk. Operators can adjust per market via the admin dashboard.

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 TierComposite ScoreAdditional Spread
newAny+0%
regular< 60+0%
regular60–80+1%
regular> 80+2%
vipAny+0%
restrictedAny+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;
}
Sharp detection is automatic. The User Scoring Engine continuously evaluates trading patterns. When a player's composite score crosses the threshold, the spread engine automatically applies wider spreads without any manual intervention. The player sees slightly worse prices but is not blocked from trading (unless they are in the restricted tier).

Example Calculations

Example 1: Normal Market (50c mid, standard user)

StepValue
Mid price50.00c
Base spread4.00%
Sharp adjustment+0%
Extreme price wideningNone (50c from edge)
Liquidity wideningNone ($100k+)
Exposure skewNone (balanced)
Final spread4.00%
Spread in cents2.00c
Ask price51.00c
Bid price49.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)

StepValue
Mid price95.00c
Base spread4.00%
Sharp adjustment+0%
Extreme price wideningdistFromEdge=5 → 1.5x multiplier
Effective spread after widening6.00%
Liquidity wideningNone ($100k+)
Exposure skewNone
Final spread6.00%
Spread in cents5.70c
Ask price97.85c (rounded)
Bid price92.15c (rounded)

Example 3: Low Liquidity Market (50c mid, $20k liquidity)

StepValue
Mid price50.00c
Base spread4.00%
Extreme price wideningNone (50c from edge)
Liquidity wideningfactor = max(0.5, 20000/50000) = 0.5 → 2x
Effective spread8.00%
Final spread8.00%
Spread in cents4.00c
Ask price52.00c
Bid price48.00c

Example 4: Sharp Bettor (50c mid, restricted tier)

StepValue
Mid price50.00c
Base spread4.00%
Sharp adjustment+3% (restricted tier)
Effective spread before widening7.00%
Extreme price wideningNone
Liquidity wideningNone
Final spread7.00%
Spread in cents3.50c
Ask price51.75c
Bid price48.25c

Example 5: Worst Case (97c mid, $10k liquidity, restricted user)

StepValue
Mid price97.00c
Base spread4.00%
Sharp adjustment+3% (restricted tier)
Spread before widening7.00%
Extreme price wideningdistFromEdge=3 → 1.7x
Spread after extreme widening11.90%
Liquidity wideningfactor = 0.5 → 2x
Spread after liquidity widening23.80% (exceeds max!)
Clamped to max15.00%
Spread in cents14.55c
Ask price99.00c (clamped at 99)
Bid price89.73c
Price bounds: Ask price is clamped at 99c (never hits 100c since a share at 100c has zero risk for the buyer). Bid price is clamped at 1c (never hits 0c). These are hardcoded safety bounds.

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.
The spread is a mathematical edge, not a gamble. As long as the mid price accurately reflects the true probability, the expected revenue per trade equals the spread. This is why sourcing accurate mid prices from Polymarket's liquid order book is critical.

All Configurable Parameters

ParameterSourceDefaultRangeDescription
defaultPctSpreadConfig41–15Base spread percentage for all markets.
minPctSpreadConfig10.5–5Minimum spread floor after all adjustments.
maxPctSpreadConfig155–25Maximum spread ceiling after all adjustments.
customSpreadPctPer-market override -1–15Overrides defaultPct for a specific market.
sharpAdjustmentUser Scoring Engine00–3Additional spread percentage for detected sharp bettors.
Extreme price thresholdHardcoded10c -Widening activates when mid price is within 10c of 0 or 100.
Liquidity thresholdHardcoded$50,000 -Widening activates when market liquidity drops below $50k.
Exposure skew thresholdHardcoded$100 -Skew activates when imbalance exceeds $100.
Max exposure skewHardcoded2c -Maximum asymmetric skew added to ask or bid.
Ask price ceilingHardcoded99c -Maximum ask price. Shares can never cost more than 99c.
Bid price floorHardcoded1c -Minimum bid price. Players always receive at least 1c per share.