Skip to main content

Payout Distribution Curves — Feature Specification

ARCH-2026-SINGLE-CURVE | February 2026


Executive Summary

Problem: Players need a transparent, predictable payout system that determines how the prize pool is divided across finishing positions in every game.

User Value: Players see exactly what they stand to win before entering a game. The tier system (Victor, Heroes, Survivors, Casualties) gives every position a battlefield identity, making even a bottom-ITM finish feel meaningful.

Business Value: Configurable curves let operators tune the risk/reward profile per game mode — casual games spread prizes wider, competitive games concentrate at the top — without changing any code.


Status Tracker

### Implementation Status
- Resolved: Single power-law formula, 3-parameter model, 4-tier classification, 5 system presets
- Pending Discussion: None
- Unclear: Cleanup migration references renamed presets (High Roller, Casual Friendly, Balanced, Competitive) that differ from original seeds (Top Heavy, Flat Distribution, Wide Field, Elite) — confirm which names are canonical
- Blocked: None

Part 1: The Formula

Three Parameters, One Curve

Every payout distribution in BattleGrid is defined by exactly three numbers:

ParameterSymbolRangeWhat It Controls
Alphaα0 < α ≤ 5.0Steepness of the curve — how much more 1st place gets vs. last paid position
ITM Percentitm%0 < x ≤ 1.0What fraction of the field gets paid (In The Money)
Floor Multiplierfloor1.0 ≤ x ≤ 3.0Minimum guaranteed payout as a multiple of entry fee

Algorithm (4 Steps)

Step 1 — Who gets paid

itmCount = FLOOR(playerCount × itmPercent)

Pure math. No minimum clamps, no maximum caps. 100 players at 30% = 30 paid. 7 players at 30% = 2 paid.

Step 2 — Reserve the floor

floorPayout = entryFee × floorMultiplier
totalFloor = itmCount × floorPayout
bonusPool = winnersPool - totalFloor

Every paid position is guaranteed at least floorPayout. If bonusPool < 0, the configuration is mathematically infeasible and the system throws a hard error — no silent fallbacks.

Step 3 — Distribute the bonus via power law

Each paid rank gets a weight:

weight(rank) = 1 / rank^alpha

The bonus pool is split proportionally:

k = bonusPool / SUM(all weights)
bonus(rank) = k × weight(rank)

Step 4 — Final payout

payout(rank) = floorPayout + bonus(rank)

All amounts rounded to 2 decimal places. Rounding remainder (cents) absorbed into 1st place so the total always balances exactly to the winners pool.

How Alpha Shapes the Curve

AlphaCurve ShapeRank 1 WeightRank 2 WeightRank 10 WeightCharacter
0.5Gentle slope1.0000.7070.316Spread evenly, more Heroes
1.0Linear decay1.0000.5000.100Balanced — the default
1.5Steep drop1.0000.3540.032Top-heavy, 1st dominates
2.0Very steep1.0000.2500.010Winner-take-most

Key insight: Alpha does not change WHO gets paid (that's ITM%). It only changes HOW MUCH each paid position gets relative to others.

Winners Pool

The formula receives winnersPool as a pre-computed dollar amount. It is not a curve parameter. The winners pool is derived from the game's fee configuration:

winnersPool = totalPool × (1 - platformFee - jackpotFee - warBondFee - hostRevenue)

The formula never computes this internally — callers pass it in as the authoritative value.


Part 2: The Four Tiers

After the formula computes every position's payout, each position is classified into a battlefield tier based on ROI (Return on Investment = payout / entryFee).

Tier Classification Rules (ARCH-2026-TIER-PLACEMENT)

TierEnum ValueRuleUI LabelColorHex
VictorVICTORRank = 1 (always)VictorHot Pink#FF1493
HeroesHEROESTop 20% of field (rank/playerCount <= 0.20)HeroesCyan#00FFFF
SurvivorsSURVIVORSITM but not Heroes (top 21-50%)SurvivorsNeon Green#39FF14
CasualtiesOTMNot in the money — bottom 50% (zero payout)CasualtiesRed#EF4444

Tier thresholds are stored in platform_config table (platform-wide, same for all games):

  • tier_heroes_percent = 0.200 (top 20%)
  • tier_survivors_percent = 0.500 (top 50% = ITM cutoff)

Classification Logic

IF rank = 1                                    → VICTOR
IF rank / playerCount <= heroesPercent (0.20) → HEROES
IF rank <= itmCount → SURVIVORS
ELSE → OTM (Casualties)

The classification happens after final payout amounts are computed. It is a label derived from placement, not ROI.

Tier Behavior at Different Game Sizes

PlayersITM Count (50%)VictorHeroes (top 20%)Survivors (21-50%)Casualties
105Rank 1Rank 2Ranks 3-5Ranks 6-10
2010Rank 1Ranks 2-4Ranks 5-10Ranks 11-20
5025Rank 1Ranks 2-10Ranks 11-25Ranks 26-50
10050Rank 1Ranks 2-20Ranks 21-50Ranks 51-100
10030Rank 1Ranks 2-16Ranks 17-30Ranks 31-100

Design note: Heroes tier is intentionally empty at small game sizes (<~10 players). The bonus pool isn't large enough to give anyone 2x returns beyond 1st place. This is correct behavior, not a bug.

What Each Tier Means to the Player

Victor — You won. Grand prize. Top of the leaderboard. Your rank is always 1 and you always exist (even in a 2-player game).

Heroes — You more than doubled your money. In a $1.00 entry game, you walked away with $2.00+. Strong finish. In larger games (50+ players), this tier covers roughly the top 5-15% of the field.

Survivors — You're in profit but didn't double up. You beat most of the field and got your entry back plus some. In a $1.00 game with a 1.01x floor, the minimum Survivor payout is $1.01.

Casualties (OTM) — Out of the money. You finished below the ITM cutoff and received nothing. Your ROI is -100%. In a 30% ITM game, this is 70% of the field.


Part 3: System Presets

Five system curves ship with BattleGrid. All are marked is_system = true and cannot be deleted by operators. Operators can create custom curves in addition to these.

Preset: Crypto Standard (DEFAULT)

ParameterValue
Alpha1.00
ITM %30%
Floor Multiplier1.01x

Character: The balanced default. Flat power law distributes prizes evenly across the top 30%. Tuned for the crypto audience — familiar to anyone who's played poker tournaments (scaled from 3-4% ITM to 30% for retention).

At 100 players, $1.00 entry, 85% winners pool ($85.00):

PositionPayoutROITier
1st~$13.7013.7xVictor
2nd~$7.107.1xHeroes
5th~$3.603.6xHeroes
10th~$2.302.3xHeroes
16th~$1.901.9xSurvivors
30th~$1.301.3xSurvivors
31st+$0.00-100%Casualties

Preset: Top Heavy

ParameterValue
Alpha1.50
ITM %30%
Floor Multiplier1.01x

Character: Steep curve. 1st place takes a massive share. Fewer Heroes because the steep decay pushes mid-ITM positions below 2x quickly. Best for competitive lobbies where players want a shot at a big score.

At 100 players: 1st place ≈ 27x. Only ~7 Heroes. 1st place gets roughly 3x what they'd get under Crypto Standard.


Preset: Flat Distribution

ParameterValue
Alpha0.50
ITM %30%
Floor Multiplier1.01x

Character: Gentle curve. Prizes spread more evenly. More Heroes (~19 at 100 players) because the gradual decay keeps more positions above 2x. 1st place is lower (~7.8x). Best for casual games where engagement matters more than a jackpot.


Preset: Wide Field

ParameterValue
Alpha1.00
ITM %40%
Floor Multiplier1.01x

Character: Same curve steepness as Crypto Standard, but 40% of the field gets paid instead of 30%. More Survivors, moderate top prizes. 1st place ≈ 10x at 100 players. Best for large casual lobbies where more players walk away happy.


Preset: Elite

ParameterValue
Alpha1.00
ITM %20%
Floor Multiplier1.01x

Character: Same curve steepness as Crypto Standard, but only the top 20% profit. Exclusive feel — 80% of the field goes home with nothing. Massive concentration at the top (1st ≈ 26x at 100 players). Best for high-stakes competitive games.


Preset Comparison Matrix

DimensionCrypto StandardTop HeavyFlatWide FieldElite
Alpha1.01.50.51.01.0
ITM %30%30%30%40%20%
Floor1.01x1.01x1.01x1.01x1.01x
1st @ 100p~13.7x~27x~7.8x~10x~26x
Heroes @ 100p~16~7~19~12~10
Survivors @ 100p~13~22~10~27~9
Casualties @ 100p7070706080
VolatilityModerateHighLowLowVery High
Best ForDefault / BalancedCompetitiveCasualLarge lobbiesHigh-stakes

Part 4: Data Pipeline

How Curves Flow Through the System

1. Admin creates a Game Preset
└── Selects a distribution_curve_id (FK → payout_distribution_curves)

2. Session is created from Preset
└── Copies distribution_curve_id onto the session record (denormalized)

3. Players join and play the game

4. Game ends → Settlement begins
└── Settlement handler reads session's distribution_curve_id
└── Loads curve via repository → maps to { alpha, itmPercent, floorMultiplier }
└── Calls DynamicPayoutFormulaService.generate(playerCount, entryFee, winnersPool, config)
└── Returns DynamicPayoutPosition[] with amounts, ROI, and band tiers

5. Payouts are distributed
└── Each position's amount is credited to the player's wallet

6. Lobby display (pre-game)
└── Use case calls formula with current playerCount to preview the curve
└── Aggregates positions into PayoutBandSummaryDTO[] (4 tiers with rank ranges + multiplier ranges)
└── Client renders PayoutCurveChart from DTO data — zero calculations

Database Schema (Post-Cleanup)

payout_distribution_curves
├── id uuid PK, auto-generated
├── name text UNIQUE, NOT NULL
├── description text nullable
├── is_system boolean DEFAULT false (true for the 5 presets)
├── is_active boolean DEFAULT true
├── alpha numeric(4,2) DEFAULT 1.00 CHECK (0 < α ≤ 5.0)
├── itm_percent numeric(4,3) DEFAULT 0.300 CHECK (0 < x ≤ 1.0)
├── floor_multiplier numeric(4,2) DEFAULT 1.01 CHECK (1.0 ≤ x ≤ 3.0)
├── created_at timestamptz DEFAULT now()
└── updated_at timestamptz DEFAULT now()

Three parameters. No zones. No clamps. No gradient exponent.


Part 5: UI Representation

Payout Curve Chart

The lobby displays a Recharts scatter plot where each ITM rank is a colored dot:

  • X-axis: Rank (reversed — Victor on the right, Casualties on the left)
  • Y-axis: Payout multiplier (ROI)
  • Zone fills: Colored background regions per tier
  • Reference lines: Breakeven line at 1.0x, Heroes threshold at 2.0x

Band Summary (Always 4 Entries)

The API always returns exactly 4 PayoutBandSummaryDTO entries, one per tier. Empty tiers have count = 0 with all numeric fields set to 0 (never undefined).

Each entry contains:

  • Tier name and player count
  • Rank range (startRank → endRank)
  • Multiplier range (minMultiplier → maxMultiplier)
  • Pool percentage allocated to the tier

Tier Display Colors

TierLine/DotZone Fill
Victor#FF1493 (Hot Pink)rgba(255, 20, 147, 0.18)
Heroes#00FFFF (Cyan)rgba(0, 255, 255, 0.15)
Survivors#39FF14 (Neon Green)rgba(57, 255, 20, 0.15)
Casualties#EF4444 (Red)rgba(239, 68, 68, 0.12)

Part 6: Edge Cases & Validation

Mathematical Constraints

ScenarioBehavior
Bonus pool < 0 (floor cost exceeds winners pool)Hard error — configuration is infeasible
ITM count = 0 (e.g., 1 player at 30%)No positions generated, empty result
ITM count ≥ player countAll players are ITM, no Casualties
2 players at 30% ITM0 ITM (floor(0.6) = 0) — edge case, consider minimum
Rounding creates ±$0.01 driftRemainder absorbed into 1st place

Tier Population Edge Cases

PlayersITM (30%)Possible Tiers Present
2-30All Casualties (degenerate — no one gets paid)
4-61Victor + Casualties only
7-92Victor + Survivors + Casualties (Heroes empty)
10-163-4Victor + Heroes + Survivors + Casualties (all 4)
17+5+All 4 tiers reliably populated

Risk Assessment

RiskProbabilityImpactMitigation
Floor cost exceeds pool at low player countsLowHighFail-fast error; UI prevents starting infeasible games
Players confused by -100% ROIMediumMediumClear tier labeling; show ITM cutoff before entry
Custom curve with extreme alpha (>3) creates near-zero payouts for mid-ITMLowMediumCHECK constraint caps alpha at 5.0; preview chart shows distribution
Rounding accumulation at 200+ playersLowLowRemainder absorption into 1st; validated via balance check

Decision Log

DateDecisionRationale
Feb 2026Single power-law curve (removed two-zone model)Simpler to audit, explain, and configure. Three parameters vs. six.
Feb 2026Removed gradient_exponent, max/min_ranked_positionsDead parameters from old two-zone model. Alpha alone controls steepness.
Feb 2026Renamed min_cash_multiplier → floor_multiplierClearer name for what it does: sets the floor guarantee.
Feb 2026Winners pool is NOT a curve parameterDerived from fee config. Storing it on the curve was a dual source of truth.
Feb 2026Crypto Standard as system defaultAlpha=1.0 is the identity curve. 30% ITM balances excitement vs. retention.
Feb 2026Heroes threshold fixed at 2.0xSimple, memorable — "did you double your money?"
Feb 2026Victor is always rank 1 regardless of ROIEven in a 4-player game where 1st gets 2.5x, they're Victor not Heroes.

Validation Checkpoints

Technical Feasibility

  • Engineering confirms implementation approach
  • No architectural conflicts identified
  • Performance requirements achievable (formula is O(n) over ITM count)

Business Alignment

  • Product Owner approves user value (transparent payouts)
  • Business metrics defined (ITM%, tier distribution, player retention per curve)
  • Confirm canonical preset names (Original seeds: Top Heavy, Flat Distribution, Wide Field, Elite)

User Experience

  • User flows are intuitive (chart + tier labels in lobby)
  • Error states handled gracefully (infeasible config = hard error, not silent)
  • Accessibility requirements met (color + label, not color alone)

File Inventory

types/src/financial.types.ts
├── DynamicPayoutFormulaConfig (3-parameter interface)
├── PayoutBandTier (VICTOR | HEROES | SURVIVORS | OTM enum)
├── PayoutBandSummaryDTO (4-entry tier summary for UI)
├── DynamicPayoutPosition (per-rank payout with tier label)
├── DynamicPayoutFormulaResult (positions + summary + config)
└── DEFAULT_DYNAMIC_PAYOUT_FORMULA_CONFIG (alpha=1.0, itm=0.30, floor=1.01)

server/src/domain/services/dynamic-payout-formula.service.ts
├── generate() (main algorithm — 4 steps)
├── simulate() (multi-player-count comparison)
├── computeRatios() (power-law weights)
└── assignBandTier() (ROI-based tier classification)

server/src/domain/repositories/payout-distribution-curve.repository.ts
├── PayoutDistributionCurve (domain entity)
├── IPayoutDistributionCurveReader (read interface)
└── IPayoutDistributionCurveWriter (write interface)

server/src/infrastructure/persistence/mappers/payout-distribution-curve.mapper.ts
└── toFormulaConfig() (DB row → { alpha, itmPercent, floorMultiplier })

client/src/components/lobby/PayoutCurveChart.tsx
├── TIER_COLOR_MAP (4 tier → hex color)
├── TIER_LABEL_MAP (4 tier → display name)
└── PayoutCurveChart (Recharts scatter plot component)

supabase/migrations/20260204000001_add_payout_distribution_curves.sql
└── Seed data for 5 system presets

supabase/migrations/20260208000001_single_curve_formula_cleanup.sql
└── Removes dead columns, renames floor_multiplier

STATUS: DRAFT