Oravan
Your line to Congress · Tu línea con el Congreso
Oravan is free, nonpartisan civic infrastructure: find your federal representatives, understand active bills in plain language (English and Spanish), get a 30-second call script, and make the call — in under 5 minutes, with no account.
MCP server
This repository also implements a remote MCP (Model Context Protocol) server — the same decoded corpus and lookups, exposed for AI assistants and agents:
- Endpoint (Streamable HTTP):
https://oravan.org/api/mcp/mcp — keyless, read-only, rate-limited; no account or credentials required
- Local/stdio:
npm ci then npx tsx scripts/mcp-stdio.mjs — the same 5 tools over stdio, zero env vars/secrets required
- Implementation:
app/api/mcp/[transport]/route.ts (built on mcp-handler + @modelcontextprotocol/sdk), tool definitions shared with the stdio entry via lib/core/mcp-tools.ts, pure data layer in lib/core/
- Five tools:
lookup_representatives, get_bill, search_bills, whats_moving, get_representative — every response carries a citation envelope (source, as-of freshness, AI-content label, license) in English or Spanish
- Official MCP Registry: published as
org.oravan/mcp (server.json at the repo root, schema-validated in CI by scripts/check-server-json.mjs)
- Docs: oravan.org/mcp (tool reference, client config, privacy posture) · docs/mcp-server-readme.md
Design principles
-
Zero accounts. ZIP code, interests, and call history live in localStorage on the visitor's device. No server-side user data exists — nothing to breach, leak, or subpoena. This is the core privacy posture for at-risk users, not a missing feature.
-
Static-first. Bills, legislators, district offices, and ZIP→district mappings are static JSON in data/, baked into ~7,500 statically generated pages (7,566 in the 2026-09-24 build, including a page for every member of Congress and a daily brief). Fast, nearly free to host, resilient under load. The server endpoints are /api/script (AI call-script generation, cached per bill + stance + language + content version in a shared store, IP rate-limited), /api/reps (pure lookup), /api/district (stateless split-ZIP address refinement: proxies the Census geocoder so the visitor's IP never reaches census.gov; the address is never stored or logged), /api/mcp (the MCP server below), /api/brand (an AI theme suggestion for a partner's site, used by the /embeds configurator, which is hidden until embeds come back), /api/stripe/webhook (partner-plan provisioning; refuses with 503 until its signing secret is set), /api/tenant/impressions (a partner reads its own embed impression counts), and /embed/portrait/[bioguide] (a same-origin portrait proxy for the embeds). Two page routes render per request — /reps (it reads the ZIP from the URL) and /nominations/[slug] — and tests/static-rendering.spec.ts names them with the reason.
-
Bilingual as a first-class feature. Full EN/ES UI via next-intl; scripts are generated in the user's language.
-
Truth first; the call is the natural next step. Oravan leads as an unbiased, plain-words account of what Congress is actually doing — understanding is the front door, never an assignment. The call apparatus stays the differentiator (voicemail legitimized, offices tally it identically; after-hours calling encouraged; district offices listed alongside DC; outcomes — spoke / voicemail / couldn't reach — logged locally on the device), and every decoded answer on a decision still open keeps a completed call script within two interactions; once the decision is over — a law, a rejected vote — the page shows the outcome and how your members voted instead. Demoted, never buried. (Amended 2026-07-26; previously "The call moment is the product." Scoped to a decision still open on 2026-09-28. Enforced by the three named invariants in tests/funnel.spec.ts.)
-
Honest about AI. Every generated summary and script is labeled at first contact, and nothing publishes unless the automated gates pass: both languages present, the official record attached, and a schema check on every decode — a decode that comes back missing a required field is discarded rather than stored half-written, and scripts/verify-sync.mjs re-checks the whole corpus and fails the nightly run before it is allowed to commit anything. (Amended 2026-08-12: one check left that file and now runs AFTER the commit — the cursor-age ceiling in scripts/check-cursor-age.mjs. It is a progress signal, not a corpus one: a stalled cursor means we are behind, and failing it before the commit made a stalled night throw away a night of already-paid decodes. Every corpus check named here is unchanged and still runs before anything is committed. See docs/constitution-log.md#cursor-age-2026-08-12.) Nonpartisan wording is a drafting instruction to the model on bill decodes and an enforced vocabulary lint on Big Questions (lib/moments-gate.mjs) — the two are not the same guarantee, and the copy never blurs them. The nightly decode path has no human step and the product never claims one; the one review it does claim is real: a caller reads, and can edit, the call script before dialing. (Amended 2026-08-06; previously "labeled, editable, and reviewed by the human before any call." See the 2026-07-25 amendment in docs/constitution-log.md, which this line should have followed and did not.)
AI content is labeled at first contact, and a call script is read — and editable — by the caller before it drives a call. That last clause is scoped to call scripts on purpose: decodes and Moment update summaries publish behind automated gates with no human step, so the label is the whole disclosure there. The label sits with the content, above the fold — never in a footnote. (Moved here from DESIGN.md on 2026-09-27, when that file was retired.)
-
Accessible by default. Semantic landmarks, skip link, visible focus, prefers-reduced-motion, 44px+ touch targets, AA contrast.
Data sources
Portraits are served from the public-domain unitedstates/images project.
Solved pipeline incidents (root cause + the CI gates that prevent recurrence) are documented in docs/solutions/.
The "Read" section (outlet-bias coverage)
Each top bill's page shows real third-party articles about it, labeled by the outlet's political lean (Left / Center / Right) — reusing AllSides' publication-level ratings, never a Oravan-invented one. Oravan takes no stance and authors no partisan text: AI is used only behind the scenes — generating each bill's news-search terms (press-style names and a subject query) and a cheap relevance gate (is this article about this bill?) — and authors nothing displayed. The ingestion runs nightly in CI and bakes results to JSON, so the site still makes zero runtime third-party calls. Without NEWS_API_KEY the sync is a no-op and the section renders nothing; a small hand-built real sample (data/coverage.json) keeps it demoable. Lean is shown by text label + position only — never party colors (a hard rule; see CLAUDE.md, rule 3).
Develop
npm run build statically generates every bill page in both locales.
Known v1 caveats
- ZIP→district mapping is ZCTA-based; a split ZIP shows all candidate districts by default (senators are unaffected). Entering a street address — optional, sent once by POST, never stored or logged — narrows it to the actual district via a server-proxied Census-geocoder lookup; the all-candidates view remains the graceful fallback whenever the geocoder can't help. The geocoder request pins the "119th Congressional Districts" layer, which needs a bump when the Census rolls the vintage to the 120th.
- The script cache and the rate limits live in shared Upstash stores (
lib/scriptcache.ts, lib/ratelimit.ts). When those stores are not configured — local dev, CI, previews — or a request to one fails, both fall back to per-instance memory rather than fail the request.
- New bills can lag behind Congress.gov: the nightly sync decodes at most
MAX_NEW_DECODES new bills per run (cost ceiling), so after a missed window the corpus catches up over several nights (decode-before-publish; the backlog drains oldest-first).
- "Read" coverage exists only for top-band bills (the long tail shows nothing); the ES locale shows the same English articles with localized chrome; outlets absent from
data/media-bias.json appear without a lean chip.
License
- Code: GNU AGPL-3.0. You may use, modify, and run this code — including as a network service — provided modified versions you operate or distribute remain open under the same license. Embedding Oravan's hosted widgets on your site via the loader/script tag does not subject your site to the AGPL; that's use of our service, not distribution of this code.
- Not licensed: the Oravan name, logo, and brand assets (
assets/brand/, app icons). All rights reserved — forks must use their own identity.
- Content: underlying legislative data is U.S. government work (public domain). AI-generated decodes and summaries are licensed CC BY 4.0, exactly as declared in the MCP citation envelope and on the citations page.