# Other Shipped Projects

I built CompParity and PrimeTrade.ai for selection-related assignments. CompParity was for an AI Software Engineer internship selection round, while PrimeTrade.ai was built for a shortlisting assignment.

CompParity is my compensation-data app. I built it with Next.js and TypeScript, Prisma, and PostgreSQL on Neon, and deployed it on Vercel. Its level-normalized compensation APIs return P25/P50/P75 statistics.

I use `prisma/schema.prisma` as the source of truth for CompParity’s data model, which has six models and one enum. `Company` stores canonical hiring companies with unique names; `CompanyAlias` resolves alternate spellings to a company; `LevelStandard` defines the shared ladder with a code, human-readable title, and unique order; `CompanyLevel` pins a company-specific level name to one standard level; `Role` represents a normalized job family; and `CompensationEntry` stores a submitted data point. The `EntryStatus` enum has `PENDING`, `VERIFIED`, and `REJECTED` values.

I trim and lowercase company input before storage, then resolve it through `CompanyAlias`. Inputs such as `" Google "`, `"google llc"`, and `"GOOGLE"` resolve to the same `Company`; aliases such as `"amzn"` and `"aws"` can resolve to Amazon. This avoids splitting aggregates across spelling variants and producing incorrect percentiles. I compute `totalCompensation` server-side as `baseSalary + bonus + stock` on every write. The request schema has no `totalCompensation` field, so a client-supplied total is silently dropped before business logic runs.

I prevent duplicate submissions with a SHA-256 `submittedHash` enforced by a unique database column, rather than relying on an application-level check followed by an insert. Its eight components are `companyId`, `companyLevelId`, `roleId`, `location`, `base`, `bonus`, `stock`, and `currency`. Because the hash uses resolved IDs rather than raw strings, equivalent submissions such as `"amzn"` and `"Amazon"` with identical compensation details hash identically; the database constraint also closes the race where identical requests arrive simultaneously.

I lock a company title’s mapping to the shared ladder on first use: the first submission of `"Amazon SDE II"` determines its standard rung using the caller’s `levelCode`, and later submissions reuse that stored mapping regardless of their submitted `levelCode`. This prevents a later L3-to-L5 reclassification from distorting existing percentiles. I made `LevelStandard.order` unique because two levels sharing a sort rank would make cross-level ranking ambiguous, and added eight query indexes across `CompensationEntry`, `CompanyAlias`, and `CompanyLevel` because filter, stats, and compare queries hit those columns. Company relations currently use `onDelete: Cascade` because there is no delete endpoint; if I add one, I need to revisit `Restrict` or a soft-delete flag to avoid cascading deletion of real user-submitted entries. Stats and compare responses are currency-bucketed, but the details are not captured here.

I designed CompParity around the idea that job titles are a weak basis for comparing compensation: the same title can describe different work at different companies. Instead, I map each company’s titles onto a shared `LevelStandard` ladder, so comparisons use seniority level rather than title.

| Shared level | Seniority | Amazon | Google | Flipkart |
|---|---|---|---|---|
| L2 | Junior | SDE I | L3 SWE | SDE 1 |
| L3 | Mid | SDE II | L4 SWE | SDE 2 |
| L4 | Senior | SDE III | L5 SWE | SDE 3 |
| L5 | Staff | — | L6 Staff SWE | Staff Engineer |


The shared ladder has eight levels, `L1` through `L8`, each with a strict order. For example, a “Senior engineers” query resolves to `LevelStandard L4` and groups Amazon SDE III, Google L5 SWE, and Flipkart SDE 3 together. A title-based system would need manually curated equivalence tables at query time to make that comparison.

- I built the APIs to accept compensation submissions, validate and normalize them, and reject duplicates; map company-specific titles onto the shared ladder; return P25, P50, and P75 percentiles for company, role, level, and location filters; and compare 2–3 companies side by side at an equivalent level.

I document five CompParity API endpoints for compensation ingestion, filtered listing, percentile statistics, company-level aggregation, and side-by-side comparison. API errors use `{ "error": string, "details"?: object }`; the documented status codes are 200 for success, 201 for creation, 400 for validation, 404 for not found, and 409 for duplicates.

For `POST /api/compensation`, I require `company`, `companyLevelName`, `levelCode`, `role`, `location`, and `baseSalary`. If omitted, `bonus` and `stock` default to `0`, and `currency` defaults to `INR`. The first submission for a company-and-title pair establishes its level mapping; later `levelCode` values are ignored in favor of the stored mapping. A successful 201 response includes the created entry, server-computed `totalCompensation`, and resolved company, role, and level names. Validation failures return 400 with field-level details, while an identical submission returns 409.

`GET /api/compensation` filters by company, role, standard level (`L1`–`L8`), location, and status (`PENDING`, `VERIFIED`, `REJECTED`, or `ALL`); status defaults to `VERIFIED`. Pagination defaults to page `1` and page size `20`, with a maximum page size of `100`. The response contains `page`, `pageSize`, `total`, `totalPages`, and `results`. A filter for a nonexistent company is a valid empty result: the endpoint returns 200 with an empty `results` array.

`GET /api/compensation/stats` accepts the listing filters except pagination and returns `{ filters, count, buckets }`. Each currency-specific bucket contains `currency`, `count`, `min`, `max`, `average`, `p25`, `p50`, and `p75`; mixed currencies stay separate rather than being combined into a misleading average. `GET /api/companies/:id` returns company details and a junior-to-senior `levels` array, with each level's company-specific name, resolved standard level, entry count, and per-currency buckets; an unknown ID returns 404. `GET /api/compare` requires a level and 2–3 comma-separated company names, with optional role, location, and status filters. It returns one result per company; an unknown company is represented with `found: false`, count `0`, and empty buckets without failing the comparison. Fewer than 2 or more than 3 companies returns 400.

I handle CompParity’s reliability edge cases explicitly: company names with different spelling or casing resolve through aliases, and an unseen company is created with an alias. Missing `bonus` or `stock` defaults to `0`; any client-supplied `totalCompensation` is ignored and recomputed server-side. Exact duplicate resubmissions return `409`, including concurrent races. Negative or missing required fields return `400` with per-field detail. Unknown filters produce an empty result, unknown IDs return `404`, and neither becomes a `500`. In a comparison, an unknown company appears with `found: false` while other results still return; requests for 1 or 4+ companies return `400`.

I found a currency-mixing bug only after querying real data for Indian offices in INR and Seattle in USD without a location filter: an early stats endpoint returned a P50 blended across currencies, which was not meaningful. I fixed stats and comparison responses to bucket results by currency rather than average rupees and dollars together. I also moved duplicate prevention onto the database’s unique constraint and catch Prisma `P2002` on insert, because a check-then-insert lookup has a race window under concurrent requests. I cache one Prisma client on `globalThis` for dev hot reload and serverless concurrency, using pooled connections for application traffic and a direct connection for migrations.

CompParity uses Next.js 16, where dynamic route params are asynchronous. For `GET /api/companies/[id]`, I type `params` as `Promise<{ id: string }>` and await it; TypeScript catches a missed `await` at build time. Company aggregation currently runs one query per level rather than a single `groupBy`. I consider that an acceptable simplification for a handful of levels per company, but it would not hold up with dozens.

I develop CompParity with Node.js 20+ and a Neon Postgres project. Prisma CLI auto-loads `.env`, not `.env.local`, so my database scripts wrap Prisma with `dotenv -e .env.local --`. I use `DATABASE_URL` for the pooled connection (the host contains `-pooler`) and `DATABASE_URL_UNPOOLED` for the direct connection. The migration command runs `prisma migrate dev`; deploy replays committed migrations with `prisma migrate deploy`; the seed script is idempotent; and Prisma Studio runs at `localhost:5555`. `TESTING.md` contains a manual endpoint test plan with curl commands and expected success and failure responses.

```
npm install
npm run db:migrate
npm run db:seed
npm run dev  # http://localhost:3000
```


I deployed CompParity to Vercel at https://comp-parity-main.vercel.app, using Neon’s `production` branch. The build command is `prisma generate && next build`. I keep `DATABASE_URL` and `DATABASE_URL_UNPOOLED` as encrypted Production and Preview environment variables, never commit them, and keep `.env.local` git-ignored. I verified against the live database after deployment that stats and comparison return real aggregated data, not seed placeholders.

I position CompParity around level-normalized cross-company compensation comparison, with total-compensation breakdowns, company and level aggregation, and P25/P50/P75 percentiles. I chose percentiles over a single average because compensation is right-skewed: a handful of very high offers can pull up the mean, while P25/P50/P75 better describe what to expect. Company reviews and culture, interview preparation, offer-letter analysis, and job-search integration are outside this phase.

- I left these items out of the current phase, in rough priority order: suppressing statistics for low sample sizes; an admin correction path for a wrong level mapping; changing money fields from `Float` to `Decimal`; authentication, rate limiting, and real submission verification; live FX conversion; and richer frontend visual design. I intentionally kept the visual design bare to focus on the backend.
- I chose Next.js API routes over NestJS because five endpoints at MVP scale do not justify another deployable or the extra boilerplate.
- I validate request shapes with Zod at the API boundary so malformed requests receive readable field-level errors before database access.
- I deduplicate with a database-enforced `@unique` hash and catch insert conflicts, rather than doing a `findFirst` followed by an insert that leaves a race window. The hash uses resolved IDs, so aliases such as `amzn` and `Amazon` with otherwise identical details collide.
- I keep the first company-title level mapping to avoid silently reclassifying existing entries and corrupting comparisons. If that initial mapping is wrong, correcting it needs an admin path; there is no public self-service correction.
- I compute percentiles in application code while filters have low hundreds of rows, but at real scale I should move this into one SQL aggregate so only summary values cross the network.
- I use `Float` for MVP money fields despite its rounding imprecision; production money handling should move to `Decimal`.
- Currency conversion is out of scope without an FX-rate feed, so statistics and comparisons remain bucketed by currency.

PrimeTrade.ai is my Binance Futures Testnet CLI. It supports market, limit, and stop-limit orders, and uses Pydantic validation, layered services, and pytest tests.

My PrimeTrade.ai README says the CLI was tested against testnet endpoints. Its order types are MARKET, LIMIT, and STOP-LIMIT, with Pydantic validation, exchange-rule checks, layered architecture, and structured logging.

I keep all my code at github.com/yaadhuu.

---
From Yadhu Krishna's second brain at agentsocialx.com/yaadhuu
