Live statistical expected-return dashboard for every Counter-Strike 2 weapon case.
Multiplies Valve's official drop odds by real-time market prices (Steam Market + CSFloat + Skinport, aggregated to the lowest live ask), averaged across wears and blended with the 10% StatTrak roll, to show what you'd actually get back per opened case. Almost every case is deeply negative — the dashboard is honest about that.
- Ranked dashboard of every case by EV %, sortable by net EV, cost, risk, or name.
- Case detail view with per-rarity-tier breakdown and per-skin price + contribution to EV.
- Side-by-side comparison for up to 4 cases (EV, σ, σ/μ lottery score, rarity-tier averages).
- Three price sources aggregated: the lowest live ask wins.
- 30-min SQLite price cache + manual "Refresh prices" button + optional cron-warmable.
- Methodology page documents every assumption and formula.
npm install
npm run fetch-metadata # optional — replaces the seed data/cases.json with the full ByMykel dump
npm run dev # http://localhost:3000The repo ships with a small seeded data/cases.json (5 cases) so the app works
out-of-the-box. Run npm run fetch-metadata once to pull every CS2 case from
ByMykel/CSGO-API.
| Command | What it does |
|---|---|
npm run dev |
Next.js dev server with HMR |
npm run build |
Production build |
npm run start |
Run the production build |
npm run fetch-metadata |
Refresh data/cases.json from ByMykel/CSGO-API |
npm run warm-cache |
Pre-fetch prices for the top-50 cases (cron-friendly) |
npm run backfill-history |
One-off Steam price-history backfill into Vercel KV |
npm test |
Run the Vitest suite |
Price history powers the /invest page, the card sparklines, the open/hold/sell
verdict chips, and the case-detail chart. It needs Vercel KV and (for deep
history) a Steam cookie.
| Var | Where to get it | Purpose |
|---|---|---|
KV_REST_API_URL / KV_REST_API_TOKEN |
Auto-set when you create a Vercel KV store and link it | Persistent price cache + history |
STEAM_LOGIN_SECURE |
Your browser cookie steamLoginSecure while logged into steamcommunity.com |
One-off deep history backfill |
CRON_SECRET |
Auto-set by Vercel Cron | Protects the daily snapshot endpoint |
After linking KV and pulling env vars locally (vercel env pull):
STEAM_LOGIN_SECURE=<cookie> npm run backfill-history
Runs gradually (rate-limited, ~1.5s/item). Only needed once — the daily cron
(/api/cron/snapshot, 06:00 UTC) keeps history current afterward, so the Steam
cookie is not needed for ongoing operation.
⚠️ STEAM_LOGIN_SECUREis an account session token. Keep it in env vars only — never commit or log it. It expires every ~1–2 weeks; re-paste a fresh one when backfill starts returning auth failures.
Locally, with no KV configured, the app reads data/history.sample.json so the
Invest UI is demoable offline.
app/ Next.js App Router pages + route handlers
api/
cases/ GET all case summaries
cases/[id]/ GET one case with full breakdown
compare/ GET ?ids=a,b,c,d
refresh/ POST invalidate cache + re-fetch
components/ UI: CaseCard, CaseGrid, CaseDetailView, CompareTable, …
lib/
metadata/ CaseMeta types + JSON loader
prices/ steam.ts, csfloat.ts, skinport.ts + aggregator.ts
cache/ better-sqlite3 30-min TTL cache
ev/ odds, calculator, variance, service
data/
cases.json committed metadata snapshot
cache.db gitignored — created at first run
scripts/
fetch-metadata.ts pulls ByMykel
warm-cache.ts cron-style pre-fetch
tests/ Vitest unit tests (ev / aggregator / cache)
See docs/superpowers/specs/2026-05-25-cs2-case-roi-design.md for the full
design spec, including the exact EV math and known approximations.
- Wear distribution is approximated as uniform across listed wears. Real Valve distribution is float-uniform within each skin's allowed range.
- Steam Market's 15% seller fee is not deducted;
bestPricealready prefers the lowest live ask across all three sources. - When all three sources fail for a skin, it's treated as $0 and the case is flagged with a "lower-bound EV" warning.
- Not financial advice. Cases are entertainment. Don't open them as an investment.
MIT.