Case study · Civic tech

Built with Claude Code: I directed the project and made the product calls; Claude wrote the code.


The purpose

Enter your address; see the upcoming committee decisions your federal representatives — a House member or delegate, plus up to two senators — are about to make, with each rep’s committee role and contact info.

Committee work is the spine, floor votes are secondary. On the floor a member is one vote of 435 or 100. On a committee marking up a bill, they’re one of about 25 who decide whether it advances at all. So the page leads with upcoming committee action you can still influence, ordered by date. Past votes and chamber-wide news are left out.

It’s position-neutral: no talking points. It shows what the decision is, who decides, when, and how to reach them. You bring the opinion.


For the user

What you do: Enter a US address or ZIP. If a ZIP spans several districts, pick yours from a list.

What you see: A section per representative (House member or delegate, up to two senators). Each shows: name, party, district, and committee roles (chair / ranking / member); tap-to-call DC and district-office phones; expandable committee dockets (the bills waiting in each committee); a date-ordered list of upcoming meetings, markups, and hearings with the rep’s role in each; and recent sponsored and cosponsored bills. Plus a “Floor this week” section and per-chamber recess notices.

What you get: Advance notice and the number to call the deciders, in time to actually reach them. Bills use the Congressional Research Service’s own summaries, verbatim. No account; your address is used once and never stored.


How it works

One lookup, step by step.

  1. Server-only. Server-rendered (Next.js on Vercel); the form calls a server action, so the API keys never reach the browser.
  2. Address → district (Geocodio). The server hashes the address (SHA-256) and uses the hash as the cache key. It checks Upstash (24h), then calls Geocodio on a miss. Only the hash is stored, so the raw address never hits the cache or a log. If it maps to several districts, it asks you to pick.
  3. District → reps (Congress.gov + open dataset). Congress.gov’s /member gives the House seat, both Senate seats, and DC-office contact. It has no member→committee endpoint, so committee roles and district-office contact come from the unitedstates/congress-legislators dataset, keyed by bioguide id. The Congress.gov DC number is a guaranteed fallback.
  4. Building a section. Per rep: committee roles; upcoming meetings and hearings matched to their committees, by date, labeled with their role; and sponsored/cosponsored bills, filtered, sorted, capped at 7. Each bill shows its CRS summary verbatim, stamped with its version date and flagged “amended since” if newer text exists.
  5. Committee dockets, on demand. Expanding a committee fetches its “Referred To” bills, newest referral first, capped at 10 — from a pre-warmed blob if there is one, else built live.
  6. Floor + recess (scraping). A scheduled job scrapes the House weekly floor XML (docs.house.gov) and the Senate floor page with cheerio. Recess is detected per chamber from the Senate schedule XML and the House weekly feed.
  7. Caching. Every fetch goes through one cached(key, ttl, loader) over Upstash, with three TTLs — events ~45 min, reference ~5h, geocode 24h. If Redis is down, it fetches live instead of failing. A nightly Vercel cron and an external GitHub Actions job (every 30 min) pre-warm the events index (Decision 3). A Congress.gov rate limiter and a per-IP + daily-credit abuse guard sit in front.

Tools & connectors

Stack

  • Next.js (App Router) on Vercel — server-rendered; keeps API keys server-side.
  • TypeScript, Tailwind, Vitest + Testing Library (195 tests, 24 files).
  • Upstash Redis — cache and store for the pre-warmed events index.
  • cheerio — parses the floor-schedule and recess scrapes.

External connectors

Connector Role
Geocodio Address → district + state
Congress.gov API Members, committee roles, meetings, hearings, bills, CRS summaries, DC contact
unitedstates/congress-legislators Committee assignments + district-office contact
docs.house.gov / senate.gov (scraped) Floor schedule + recess signal
Vercel Cron + GitHub Actions Cache-warming (nightly + every 30 min)

No LLM (retired — Decision 1). Bill text is the CRS summary, verbatim; no model in the request or build path.


Decisions

Decision 1 — Retire the LLM entirely

Session 2026-07-08-6 · 2026-07-08

Reviewing the pipeline, I caught the model writing a bill’s summary from its title alone, making up the description when it couldn’t retrieve it. My call: don’t try to police the model into behaving; where there’s no trustworthy source, replace the model with a deterministic tool. Claude built that fix — bills render the CRS summary (the nonpartisan experts’ own plain-English text) verbatim, and show structured-only (title + link) when there’s none. That left the model no remaining job, so it went entirely, taking the Anthropic dependency and the only per-call cost with it.

“Does this mean it sometimes writes just based on the title, or no? It must never do that.” — Lukas, 2026-07-08-6, 01:34 UTC

Decision 2 — Committees as the spine (and the “moat” framing)

Session 2026-07-09-10 · 2026-07-09 · Issue #24

An open issue asked the blunt question: why use this instead of Congress.gov? The answer: Congress.gov has the facts, the value of Representative Tracker is selection and ordering. It surfaces the ~25-person committee decisions your reps face, soonest first, with the number to call. It centers committee work. The “routing, not content” framing was Claude’s, which I approved; separately, I originated the committee-docket feature and drew the line to keep that strategic view out of the user-facing copy.

“One piece of additional utility I am considering… A feature that lets you select the committees a representative is in and see which bills are ‘stuck’ in that committee waiting to be sent to the floor.” — Lukas, 2026-07-09-10, 21:21 UTC

“Good work on the framing, that is how I see things but ought not make it into the user-facing description.” — Lukas, 2026-07-09-10, 21:23 UTC

Decision 3 — Stay on free tiers, and warm the caches within the limits

Sessions 2026-07-09-3 · 2026-07-09-6 · 2026-07-09

Before building the pre-warm cron, I set the rule: everything stays on free tiers, for good. That hit Vercel Hobby’s ~60s function limit, which can’t warm all 535 members in one pass. So Claude built a convergent-cursor index: re-sweep the recently-updated head plus advance a per-chamber cursor, covering everything over ~7–10 nights, in one small blob.

“Yes, add the note. Consider that I am on free tier for everything.” — Lukas, 2026-07-09-3, 05:46 UTC

Decision 4 — How often to call Congress.gov and refresh the caches

Sessions 2026-07-09-6 · 2026-07-09-16/-17 · 2026-07-09 · Issue #23

The pre-warm was built, tested, and deployed. I asked: Does it surface information soon enough to matter? Is the bottleneck Representative Tracker’s refresh cadence or the Congress.gov API? Claude built the mechanism to answer it: the tool can’t be fresher than the source, so measure the source’s own publish lag before setting a cadence. It lags the House source by ~15 min, so an external GitHub Actions scheduler runs every 30 min.

“Before we do both, I ask you: How badly is the free tier reducing the effectiveness of this project at reaching the stated goal of knowing what representatives are doing soon enough for it to matter?” — Lukas, 2026-07-09-6, 18:19 UTC

“File this as a new issue, but first consider that iirc the congress API is also a limiter here.” — Lukas, 2026-07-09-6, 18:22 UTC

“Start the measurement.” — Lukas, 2026-07-09-16/-17, 23:05 UTC

Decision 5 — Make the privacy claim true, not just well-worded

Session 2026-07-10-3 · 2026-07-10

Auditing the privacy disclaimer, Claude found the geocode cache was keyed by the raw address, so the promise “we don’t store your personal information” wasn’t true. Rather than soften the copy, hash the address (SHA-256) before it becomes the key, making the claim literally true: the raw address never reaches Upstash or a log. The hash was Claude’s proposal; I approved it and sharpened the user-facing wording.

“Not putting the address in the cache sounds sensible.” — Lukas, 2026-07-10-3, 20:09 UTC

“I also don’t like the copy of ‘no analytics beyond Vercel’s built-in’ because the user probably doesn’t even know what a vercel is.” — Lukas, 2026-07-10-3, 20:09 UTC


Timeline — day by day

From the 43 journal entries in journal/. “Sessions” = journal entries filed that day. Built 2026-07-08 → 2026-07-16.

2026-07-08 · 6 sessions. Kickoff plus the whole MVP skeleton. Scaffolded the repo and workflow, adopted the spec, filed the MVP milestone and Issues #1–#11. Built the Next.js/Tailwind/Vitest app, address → reps lookup (Geocodio + Congress.gov) with disambiguation, and the full per-rep layout. Built a Haiku summary pipeline, then retired the LLM the same night for verbatim CRS summaries (Decision 1).

2026-07-09 · 19 sessions. The big build-and-ship day. Upstash caching with graceful degradation (#7); deployed to production with footer, privacy, and feedback (#10, #11). Hit the ~60s function ceiling and built the convergent-cursor pre-warm index (#16, Decision 3). Filed strategy issues (#24/#25/#26); fixed the event-link trust bug (#20); held the “why not Congress.gov” session (#24, Decision 2). Added the rate limiter (#17) and the external scheduler, measured the ~15-min source lag, and set 30 min (#23, Decision 4). Replaced district-office scraping with an open dataset (#13). Built per-chamber recess behavior and verified it in production (#8/#27).

2026-07-10 · 5 sessions. Accuracy and honesty. Reworded the pitch for precision (#30). Turned the privacy audit into the SHA-256 cache-key fix (#31, Decision 5). Met the accessibility bar — Lighthouse 100, axe 0, keyboard + VoiceOver (#9).

2026-07-12 · 1 session. Weekly reconciliation — regenerated the MAP.md tree and reconciled its annotations.

2026-07-13 · 1 session. Reconciliation wrap-up: walked the repo with the owner, wrote a week-in-review, and filed #32 to standardize session labels.

2026-07-14 · 5 sessions. Feedback and design. Triaged feedback into five issues; built the Capitol-dome favicon (#18); ran the design pass off default Tailwind (#25); added policy tags and floor-bill CRS summaries (#36/#37); polished the floor section (#33/#34).

2026-07-15 · 4 sessions. Shipped the committee docket (#21, the feature from Decision 2). Declined address autocomplete on cost (#35 → #41). Re-audited accessibility, fixed a placeholder, verified committee links (#38/#42/#40). Ran the compliance review with four research agents — substantially compliant, one copy fix (#26).

2026-07-16 · 2 sessions. Cleared the enhancement backlog — privacy page (#43), footer warranty + attribution (#44), abuse guard (#41), hidden empty dockets (#39). Reclassified the “pre-launch gates” as accepted risk and a don’t-regress invariant. MVP complete.


Conclusion

It started as an idea: a more effective way to follow what your representatives are about to do. It became a usable tool, wired into Geocodio, the Congress.gov API, and open congressional datasets, and live at reptracker2.vercel.app. Every decision above was mine; Claude turned them into working code.