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:
| Parameter | Symbol | Range | What It Controls |
|---|---|---|---|
| Alpha | α | 0 < α ≤ 5.0 | Steepness of the curve — how much more 1st place gets vs. last paid position |
| ITM Percent | itm% | 0 < x ≤ 1.0 | What fraction of the field gets paid (In The Money) |
| Floor Multiplier | floor | 1.0 ≤ x ≤ 3.0 | Minimum 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
| Alpha | Curve Shape | Rank 1 Weight | Rank 2 Weight | Rank 10 Weight | Character |
|---|---|---|---|---|---|
| 0.5 | Gentle slope | 1.000 | 0.707 | 0.316 | Spread evenly, more Heroes |
| 1.0 | Linear decay | 1.000 | 0.500 | 0.100 | Balanced — the default |
| 1.5 | Steep drop | 1.000 | 0.354 | 0.032 | Top-heavy, 1st dominates |
| 2.0 | Very steep | 1.000 | 0.250 | 0.010 | Winner-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)
| Tier | Enum Value | Rule | UI Label | Color | Hex |
|---|---|---|---|---|---|
| Victor | VICTOR | Rank = 1 (always) | Victor | Hot Pink | #FF1493 |
| Heroes | HEROES | Top 20% of field (rank/playerCount <= 0.20) | Heroes | Cyan | #00FFFF |
| Survivors | SURVIVORS | ITM but not Heroes (top 21-50%) | Survivors | Neon Green | #39FF14 |
| Casualties | OTM | Not in the money — bottom 50% (zero payout) | Casualties | Red | #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
| Players | ITM Count (50%) | Victor | Heroes (top 20%) | Survivors (21-50%) | Casualties |
|---|---|---|---|---|---|
| 10 | 5 | Rank 1 | Rank 2 | Ranks 3-5 | Ranks 6-10 |
| 20 | 10 | Rank 1 | Ranks 2-4 | Ranks 5-10 | Ranks 11-20 |
| 50 | 25 | Rank 1 | Ranks 2-10 | Ranks 11-25 | Ranks 26-50 |
| 100 | 50 | Rank 1 | Ranks 2-20 | Ranks 21-50 | Ranks 51-100 |
| 100 | 30 | Rank 1 | Ranks 2-16 | Ranks 17-30 | Ranks 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)
| Parameter | Value |
|---|---|
| Alpha | 1.00 |
| ITM % | 30% |
| Floor Multiplier | 1.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):
| Position | Payout | ROI | Tier |
|---|---|---|---|
| 1st | ~$13.70 | 13.7x | Victor |
| 2nd | ~$7.10 | 7.1x | Heroes |
| 5th | ~$3.60 | 3.6x | Heroes |
| 10th | ~$2.30 | 2.3x | Heroes |
| 16th | ~$1.90 | 1.9x | Survivors |
| 30th | ~$1.30 | 1.3x | Survivors |
| 31st+ | $0.00 | -100% | Casualties |
Preset: Top Heavy
| Parameter | Value |
|---|---|
| Alpha | 1.50 |
| ITM % | 30% |
| Floor Multiplier | 1.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
| Parameter | Value |
|---|---|
| Alpha | 0.50 |
| ITM % | 30% |
| Floor Multiplier | 1.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
| Parameter | Value |
|---|---|
| Alpha | 1.00 |
| ITM % | 40% |
| Floor Multiplier | 1.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
| Parameter | Value |
|---|---|
| Alpha | 1.00 |
| ITM % | 20% |
| Floor Multiplier | 1.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
| Dimension | Crypto Standard | Top Heavy | Flat | Wide Field | Elite |
|---|---|---|---|---|---|
| Alpha | 1.0 | 1.5 | 0.5 | 1.0 | 1.0 |
| ITM % | 30% | 30% | 30% | 40% | 20% |
| Floor | 1.01x | 1.01x | 1.01x | 1.01x | 1.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 @ 100p | 70 | 70 | 70 | 60 | 80 |
| Volatility | Moderate | High | Low | Low | Very High |
| Best For | Default / Balanced | Competitive | Casual | Large lobbies | High-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
| Tier | Line/Dot | Zone 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
| Scenario | Behavior |
|---|---|
| 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 count | All players are ITM, no Casualties |
| 2 players at 30% ITM | 0 ITM (floor(0.6) = 0) — edge case, consider minimum |
| Rounding creates ±$0.01 drift | Remainder absorbed into 1st place |
Tier Population Edge Cases
| Players | ITM (30%) | Possible Tiers Present |
|---|---|---|
| 2-3 | 0 | All Casualties (degenerate — no one gets paid) |
| 4-6 | 1 | Victor + Casualties only |
| 7-9 | 2 | Victor + Survivors + Casualties (Heroes empty) |
| 10-16 | 3-4 | Victor + Heroes + Survivors + Casualties (all 4) |
| 17+ | 5+ | All 4 tiers reliably populated |
Risk Assessment
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Floor cost exceeds pool at low player counts | Low | High | Fail-fast error; UI prevents starting infeasible games |
| Players confused by -100% ROI | Medium | Medium | Clear tier labeling; show ITM cutoff before entry |
| Custom curve with extreme alpha (>3) creates near-zero payouts for mid-ITM | Low | Medium | CHECK constraint caps alpha at 5.0; preview chart shows distribution |
| Rounding accumulation at 200+ players | Low | Low | Remainder absorption into 1st; validated via balance check |
Decision Log
| Date | Decision | Rationale |
|---|---|---|
| Feb 2026 | Single power-law curve (removed two-zone model) | Simpler to audit, explain, and configure. Three parameters vs. six. |
| Feb 2026 | Removed gradient_exponent, max/min_ranked_positions | Dead parameters from old two-zone model. Alpha alone controls steepness. |
| Feb 2026 | Renamed min_cash_multiplier → floor_multiplier | Clearer name for what it does: sets the floor guarantee. |
| Feb 2026 | Winners pool is NOT a curve parameter | Derived from fee config. Storing it on the curve was a dual source of truth. |
| Feb 2026 | Crypto Standard as system default | Alpha=1.0 is the identity curve. 30% ITM balances excitement vs. retention. |
| Feb 2026 | Heroes threshold fixed at 2.0x | Simple, memorable — "did you double your money?" |
| Feb 2026 | Victor is always rank 1 regardless of ROI | Even 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