Because a job title tells you nothing — a level tells you everything.
Salary data modeled around the tuple company → level → role → location, with levels made comparable across companies via a universal seniority rank (1–10). So you get a real answer to "Is Google L4 the same as Amazon SDE-2?" (yes — both rank 4).
- Level Equivalence Engine (
src/lib/levels.ts) — every company label (L4, SDE-2, E4, IC3, MSFT 62) maps to a universal rank. Comparison groups by rank, not title. - Data Trust Score (
src/lib/comp.ts) — every company×level aggregate carries a 0–100 score from sample size, freshness, and IQR spread, surfaced as 🟢🟡🔴. Groups with < 3 entries are hidden.
Next.js (App Router) · TypeScript · Prisma · PostgreSQL (Neon) · Zod · Recharts
npm install
cp .env.example .env # paste your Neon connection string into DATABASE_URL
npm run db:generate # prisma generate
npm run db:migrate -- --name init
npm run db:seed # ~400 realistic entries
npm run dev # http://localhost:3000
# in a second terminal, hammer the API edge cases:
npm run test:api| File | Responsibility |
|---|---|
normalize.ts |
company-name + level + role normalization → slugs |
levels.ts |
universal level→rank mapping (the differentiator) |
comp.ts |
total-comp formula, percentiles, aggregation, trust score, dedup fingerprint |
validation.ts |
Zod schemas at every API boundary |
ingest.ts |
shared write path: validate → normalize → rank → compute → dedup → store |
Total comp is computed at write-time and stored for fast reads:
TC = base + stockGrant/vesting + (targetBonus + signOn/vesting). Missing
stock/bonus default to 0. Dedup is a SHA-256 fingerprint over the entry's
identifying fields (unique DB column → duplicate single submit returns 409;
duplicate CSV rows are skipped and reported, batch still succeeds).
| Method | Route | Purpose | Status |
|---|---|---|---|
| POST | /api/salaries |
ingest one entry | 201 400 409 |
| GET | /api/salaries |
list + filter + sort + paginate | 200 |
| POST | /api/salaries/import |
CSV bulk import | 200 400 |
| GET | /api/companies/[slug] |
aggregated comp by level + trust | 200 404 |
| GET | /api/compare?companies=a,b |
apples-to-apples by rank | 200 400 |
| POST/GET | /api/comparisons |
save / list (auth) | 200 201 401 |
Auth for saved comparisons is intentionally thin (an x-user-email header /
cookie identifies the user) so the flow is demonstrable without a full auth
provider; swap in a real session in production.
npm run test:api— curl edge cases: valid, missing-stock, duplicate (409), negative/garbage input (400), name normalization, unknown company (404), compare arity, auth gating.- Core logic has been unit-verified (normalization, TC, percentiles, trust, dedup, level equivalence).