Web app · NBA salary cap
The Cap Room
Explore NBA team salaries, compare players, and test two-team trades against the 2023 collective bargaining agreement. Every rule shows its math.
- Role
- Timeline
- Stack
- Links
By the numbers
44
engine tests, including payrolls one dollar over every apron and tax line
19
rule checks in a single detailed trade verdict
582
players in the dated league statistics snapshot
9
team rosters checked by the engine against published totals, to the dollar
Before the app runs
- On my machine etl
/pull_stats.py A Python ETL pulls the league stats. stats.nba.com blocks cloud servers, so this never runs in production. - In the repo Dated snapshot 582 players, plus 9 team rosters.
- Before every build scripts
/validate-data.ts Recalculates each roster total with the engine’s salary-cap rules. A one-dollar difference from the published total fails the build.
- tradeRules.ts
- capsheet.ts
- picks.ts
- constants.ts
On every trade check
- Browser The URL stores the trade Every trade is a shareable link that reopens the same deal.
- API POST /api
/trade Checks the body, loads the cap sheets and picks, and calls validateTrade./validate - Response An itemized ledger A legal or illegal verdict, followed by each rule and its calculations. The sample trade returns 19 rows.
The idea
The Cap Room helps you work through NBA salary-cap decisions. Browse team cap sheets, compare players by league percentile and check two-team trades against the 2023 collective bargaining agreement (CBA). Each trade verdict shows the rule checks and salary calculations that support it.
What I built
I built the CBA engine in pure TypeScript. Every import in engine/ points to another engine file, keeping it independent of React, Next.js and I/O. It implements the 2023 rules for salary matching and cash, including the $209,015,000 first apron, $250,000 buffer and $8,495,000 annual trade-cash limit. The API returns one row per rule with the relevant calculations.
A complete trade proposal fits in a shareable URL. The interface uses the same REST routes available through curl. In October, I upgraded to Next.js 16 and React 19 and redesigned the app with a phone-friendly league board, one-click sample trades with expected verdicts, and Cmd-K player search that ignores accents.
I process league stats offline with a Python ETL pipeline and commit dated snapshots. On every push to main, CI checks types, lint, engine tests, data validation and a production build.
The hard parts
-
Breaking down the CBA rules
CBA rules affect one another. The engine gives each rule a small check with a pass or fail result and its own calculations. Apron restrictions apply only above the threshold, so a payroll exactly on the line remains under it. Tests cover payrolls one dollar over each line.
-
Data that has to add up
The engine recalculates each seeded roster’s cap total and compares it with the published team total. A one-dollar difference fails the build, catching mismatches between the data and the engine.
-
Keeping trade verdicts in sync
Moving the cash slider can create several proposals per second. Validation waits 220 ms, cancels the previous request and pairs each response with its proposal. The last verdict stays dimmed until the current one arrives, preventing an older response from validating a newer trade.
Before and after
| Before | After | Source | |
|---|---|---|---|
| Framework | Next.js 14, with a critical security advisory that had no 14.x fix. | Next.js 16 and React 19; npm audit of the runtime dependencies reports 0. | Commit ee79ff3, October 9, 2026 |
| Accessibility (axe-core) | 6 failing rules across about 190 elements, including faded text and a 3.6:1 contrast ratio for the red FAIL label. | 0 violations across 75 axe-core 4.14 runs, covering every page with panels and menus open at widths from 1440 to 320 px. | Commits 189d4f0 and 24b396f, October 9, 2026 |
| The league board on a phone | A cropped chart that scrolled sideways. | Horizontal bars that fit a 390 px screen, with the cap, tax and apron lines marked. | Commit 189d4f0, October 9, 2026 |
What’s next
Three-team trades, pick ledgers for every team, and a fresh roster snapshot.
Try it
Shared approaches
Keep rules separate from the interface
Test against real runtimes and data