This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OSP-API is a free, open-source REST API aggregating congressional accountability data — campaign finance, stock trades, voting records, travel disclosures, and legislation. Licensed under AGPL-3.0.
- Framework: Next.js 16 App Router (TypeScript)
- Data source: CongressWatch static JSON over CDN — no database in the data path
- API keys / accounts: Neon Postgres (Drizzle ORM) + Neon Auth (Managed BetterAuth)
- Rate limiting: Redis via
@upstash/ratelimit(sliding window, per tier) — fails open if unavailable - Email: Resend, for delivering the API key on issuance
History (read this before touching the data layer): this API originally ran on two Supabase projects — one for
api_keys+ GoTrue auth, one holding the congressional data. Both were deleted. The Upstash Redis instance was deleted too. Do not reintroduce@supabase/supabase-js,lib/supabase.ts, or anySUPABASE_*env var; they are gone deliberately.
npm run dev # Start dev server
npm run build # Production build
npm run start # Start production server
npx tsc --noEmit # Typecheck (use this — `npm run lint` is broken; Next 16 removed `next lint`)
npx drizzle-kit push # Apply lib/schema.ts to NeonCongressWatch pipeline → commits JSON to git → congresswatch.vercel.app/data/*.json (CDN, public, CORS)
↓ fetch
OSP-API: auth · tiers · rate limits · score breakdown
lib/congress.ts— fetches CongressWatch JSON (members.json,details/{id}.json,stats.json). Types + filter/sort/paginate helpers. This replaced the Supabase data client.lib/aggregate.ts— builds the cross-member trades/bills index for the global endpoints, which upstream only exists inside per-member detail files. Fans out over 536 files, cached 1h.lib/scoring.ts— TypeScript port of CongressWatch'scompute_score()(fetch_finance.py:312), returning the 6 components. Uses Python's banker's rounding to stay faithful.lib/db.ts/lib/schema.ts— Neon Postgres via Drizzle. Module-scopepg.Pool+attachDatabasePool(required for Vercel Fluid compute).lib/auth.ts—validateApiKey():X-API-Key→ Neon lookup → rate limit → usage increment.lib/redis.ts— tiered rate limiters. ReadsKV_REST_API_*orUPSTASH_REDIS_REST_*.lib/neon-auth.ts/lib/neon-auth-client.ts— Neon Auth server + browser clients.lib/handlers.ts—memberCollection()factory shared by the five member sub-collection routes.lib/response.ts—ok(),paginated(),err(),options()with CORS baked in.
- Response envelope:
{ data, meta: { page, per_page, total } }for lists,{ data }for singles,{ error }for errors - Native schema: field names are CongressWatch's, passed through unchanged. Do not rename them.
- Pagination: in-memory slice;
per_pagemax 100 - Auth-required routes: call
validateApiKey(request)first; on failure returnerr(auth.error!, auth.status ?? 401)— the status matters, rate-limit rejections are 429, not 401 - Public routes:
/v1/members(list) and/v1/statsonly - CORS: all responses include
Access-Control-Allow-Origin: *via response helpers
- Detail files are flat — there is no
financeordonorssub-object.total_raised,cash_on_hand,top_donors_listare top-level. cash_on_handis a string ("$1,234,567"), not a number.- Trade
amountis a string bracket ("$15,001 - $50,000") and is null on ~231 rows. districtis a string,""for senators.total_raisedis null for 69 of 536 members.- Coverage is uneven:
tradesin 124/542 files,travelin 307,cash_on_handin 401. Always use defaults. - Dates are mostly ISO but legacy trade/travel rows can be
MM/DD/YYYY— normalize withparseDateAny()before comparing. - The stored
scorecan lag its own inputs: CongressWatch recomputes it only in its finance job, while votes/trades/travel refresh on separate crons./scorereturnsstored,total, anddriftto expose this.
Update CHANGELOG.md on every change: semver + date + description.