All Projects
Case Study Personal Project · 2026 Civic Tech · $0 Infra

Who Reps Me?
Every official who represents you, one address at a time.

In April 2025, Google shut down the Civic Information API. It was the only free nationwide way to look up a city council member or school board rep, and when it went away nobody replaced it. That bothered me enough to spend a few weekends rebuilding the lookup myself, federal down to city hall, on infrastructure that costs me nothing to keep running.

Role
Solo — Design, Build,
Data Pipeline
Timeline
2026
Coverage
24 states ·
83 jurisdictions
Infra Cost
$0/month
Stack
React · Netlify Functions
GitHub Actions

The Problem

Google walked away.
The paid vendors didn't blink.

For over a decade, Google's Civic Information API was the quiet backbone behind most "find your representative" tools on the web — free, nationwide, and the only source that reached down to city council and school board level. In April 2025, Google shut it down.

Every provider left standing charges for it. Cicero and BallotReady both sell commercial civic data licenses — reasonable products, but not something a free public lookup tool can build on. Federal data is still easy (Congress publishes it). State data is workable (most legislatures publish open data). Local data — the level that actually governs zoning, trash pickup, and school boards — had no free nationwide source left at all.

"I went looking for a free replacement and kept finding the same thing: a tool that covered federal and state fine, then asked for money at exactly the level I cared about. Most of them had been quietly running on Google's API the whole time."

24
States with live coverage
83
Jurisdictions indexed
450
Officials in the database
$0/mo
Infrastructure spend

How It Works

Type an address.
Get everyone who represents you.

One search box, three levels of government. Federal senators and representatives, state legislators and executives, and the city council members and school board reps most "find your rep" tools don't even attempt to cover.

Live demo — search by address, then browse results grouped by LOCAL / STATE / FEDERAL.

Data Sources

Four sources.
Zero paid APIs.

No single free source covers all three levels of government, so the pipeline stitches together four of them — one per layer, plus geocoding to route the address to the right district.

🏛️
Federal

US House & Senate

Via 5calls' free public API — no key required, generous limits, built for exactly this use case.

5calls API · free
🏢
State

Legislators & Executives

Via Open States v3 — state house, state senate, governor, and other statewide offices.

OPENSTATES_API_KEY · free
🏙️
Local

City & County Officials

No API exists for this layer, so I built a scraper that reads municipal websites and uses an LLM to pull structured official records out of whatever it finds. This is the part that took the longest by a wide margin.

Own scraper · free
📍
Geocoding

Address → Coordinates

US Census Geocoder first, Nominatim (OpenStreetMap) as fallback whenever Census can't resolve the query.

Census + Nominatim · no key

Architecture

Search data flow.

One address in, three lookups run in parallel, one merged response rendered by level of government. Geocoding is the fork in the road — get it wrong and everything downstream degrades.

flowchart TD A["Address input"] --> B["Geocode"] B -->|"Full street address"| C["US Census Geocoder"] B -->|"Bare ZIP / City fallback"| D["Nominatim"] C --> E["Lat/Lng + district IDs"] D --> E E --> F["5calls: Federal"] E --> G["Open States: State"] E --> H["Local officials: shard or on-demand scrape"] F --> I["Merge results"] G --> I H --> I I --> J["Rendered by section: LOCAL / STATE / FEDERAL"]

Field Notes

The bare-ZIP bug nobody warns you about.

The Census Geocoder's onelineaddress endpoint is an address-range matcher, not a general-purpose geocoder — it needs an actual street number and street name to match against. Feed it a bare city name or a ZIP code alone and it returns zero matches, even though the search box invites "address or zip code." Without a fallback, that silently degrades a bare-city/zip search down to federal-only results: two senators and a representative, no error message explaining why the state legislator and city council never showed up.

1

Detect the shape of the query

Before ever calling the Census Geocoder, the query is classified: a full street address, a bare 5-digit ZIP, or a "City, State" pair. Only a full address goes straight to the address-range matcher.

2

Bare ZIP → zippopotam.us

A 5-digit ZIP with nothing else routes to zippopotam.us, a free lookup that returns a ZIP's centroid lat/lng directly — no street address required, no matching against a range.

3

Bare "City, State" → Nominatim, then Census coordinates

A city-only query resolves through Nominatim (OpenStreetMap's free geocoder) to a lat/lng, which is then passed to the Census Bureau's coordinates endpoint — a different endpoint than onelineaddress, one built to accept raw coordinates instead of a street-range match — to recover the district and jurisdiction data the address endpoint would have provided.

Address autocomplete suggestion dropdown

Autocomplete nudges users toward a full street address before they ever hit the bare-ZIP edge case.

Field Notes

Municipal websites are not data.

The local layer is the whole reason this project exists, and it's the part that fought me hardest. There is no schema. One city publishes its council on a clean roster page. The next one puts it in a PDF of last year's meeting minutes. The next one has a photo of the council with the names in the caption. A few still list a member who lost an election two cycles ago.

My first pass was ordinary scraping — find the table, find the list, parse the names. It worked on maybe a third of the jurisdictions I tried and broke on the rest, and every fix I made for one city broke the assumption I'd made for another. Writing a parser per city was never going to finish.

What actually worked was inverting it: stop trying to parse the page, and hand the page to an LLM with a strict output schema instead. The model is good at the thing that was defeating me — reading an unfamiliar layout and understanding that "Place 4" and "District 4" and "Ward 4" all mean the same thing. That moved the hard problem from parsing to verification, which is a much better problem to have, because a wrong answer at least looks like an answer I can check.

It also meant I couldn't trust the output. An LLM reading a stale page will confidently give you a stale council member. So nothing goes live from the scraper alone — every shard gets reviewed before it's committed, and the crowdsourced correction path below exists because I already knew the data would be wrong in places I'd never notice on my own.

Honest caveat: 24 states, not 50, is exactly this problem. Local coverage expands at the speed I can review it, not the speed the scraper can run.

Crowdsourcing

The database that fixes itself.

Local officials data goes stale fast — elections, resignations, redistricting, a city council seat that flips mid-term. Instead of an admin dashboard nobody but me would ever open, corrections route through GitHub, where they're triaged automatically before a human ever has to look.

flowchart TD A["User clicks 'Suggest an official'"] --> B["Submits a link + note"] B --> C["LLM triage"] C --> D["Duplicate of an open
user-reported issue?"] D -->|"Yes"| E["Comment on existing GitHub issue"] D -->|"No"| F["Open new GitHub issue"] E --> G["Appended to tracking file"] F --> G G --> H["Human review"]
Suggest an official submission modal

The crowdsource submit modal — a link and a note is all it takes.

Infrastructure Philosophy

A JSON file is the database.

There's no database server and no always-on worker. A per-state JSON shard committed to the git repo is the database — versioned, diffable in a pull request, free to host. A GitHub Actions cron job does the work a background service normally would, waking on a schedule to re-scrape and re-validate one shard at a time. Netlify serves the results from the edge with no origin compute behind them. The data is small enough and changes slowly enough that a real database would have been a bill I took on for nothing.

"The constraint drove the architecture, not the other way around. This is a side project nobody pays for, so anything with a monthly bill was off the table from the start — which turned out to be a useful forcing function."

Coverage

24 states and counting.

Federal and state coverage is nationwide from day one — Open States and 5calls both cover all 50 states. Local coverage is what's still expanding: each state's local-officials shard has to be built, scraped, and verified one jurisdiction at a time.

Washington — Live coverage WA Oregon — Live coverage OR California — Live coverage CA Idaho — Coming soon ID Nevada — Live coverage NV Utah — Live coverage UT Arizona — Live coverage AZ Montana — Coming soon MT Wyoming — Coming soon WY Colorado — Live coverage CO New Mexico — Coming soon NM North Dakota — Coming soon ND South Dakota — Coming soon SD Nebraska — Coming soon NE Kansas — Coming soon KS Oklahoma — Live coverage OK Texas — Live coverage TX Minnesota — Coming soon MN Iowa — Coming soon IA Missouri — Coming soon MO Arkansas — Coming soon AR Louisiana — Coming soon LA Wisconsin — Coming soon WI Illinois — Live coverage IL Kentucky — Coming soon KY Tennessee — Live coverage TN Mississippi — Coming soon MS Michigan — Live coverage MI Indiana — Live coverage IN West Virginia — Coming soon WV Virginia — Live coverage VA North Carolina — Live coverage NC Alabama — Coming soon AL Ohio — Live coverage OH Pennsylvania — Live coverage PA Maryland — Live coverage MD Washington, D.C. — Coming soon DC South Carolina — Coming soon SC Georgia — Live coverage GA New York — Live coverage NY New Jersey — Live coverage NJ Delaware — Coming soon DE Florida — Live coverage FL Vermont — Coming soon VT Massachusetts — Live coverage MA Connecticut — Coming soon CT Maine — Coming soon ME New Hampshire — Coming soon NH Rhode Island — Coming soon RI Alaska — Coming soon AK Hawaii — Live coverage HI

24 of 51 states and territories currently have live coverage: Washington, Oregon, California, Nevada, Utah, Arizona, Colorado, Oklahoma, Texas, Illinois, Tennessee, Michigan, Indiana, Virginia, North Carolina, Ohio, Pennsylvania, Maryland, Georgia, New York, New Jersey, Florida, Massachusetts, Hawaii.

Live coverage
Coming soon

Screens

What it looks like.

LOCAL Section
LOCAL Section
City council, school board, and county officials — the layer with no free API, built from scratch.
STATE Section
STATE Section
State house, state senate, and executive offices, pulled live from Open States v3.
Federal Official Detail
Federal Official Detail
Senator Maria Cantwell's card — committees, bio, and office locations in one view.
Mobile Viewport
Mobile Viewport
Full results at 375×812 — the layout most users will actually see it at.

How I Built This

I didn't write most of this code.

Worth saying plainly, because it's the honest version and because how I work is part of what I'm showing here. I'm a product designer, not a software engineer. Nearly all of the code in this project was written by AI tooling — Claude and Cursor — working from my direction.

What I did was everything around that. I decided what the product was and who it was for. I chose the four data sources and worked out which layer each one could actually cover. I made the call that a JSON file in git would stand in for a database, and that corrections should route through GitHub instead of an admin panel. I designed every screen. And when the results came back wrong — a bare ZIP silently returning federal-only, a scraper confidently inventing a council member — I'm the one who noticed, worked out why, and decided what the fix should be.

I'd rather tell you that up front than have it come out in an interview. If what you need is someone who can write a red-black tree on a whiteboard, that isn't me. If what you need is someone who can take a vague problem, decide what should exist, and drive it all the way to a deployed thing real people can use, that's the entire point of this page.

"The model handles syntax. Deciding what to build, noticing when it's wrong, and knowing what wrong even looks like — that part didn't get automated."

Outcome

What it produced.

🗳️

450 officials indexed

Federal, state, and local officials, unified into one searchable dataset.

🗺️

24 states covered

Live local-officials coverage, with federal and state coverage nationwide.

🏛️

83 jurisdictions

Cities and counties with a scraped, human-reviewed local officials shard.

💸

$0/month recurring cost

No database bill, no server bill, no API bill — every dependency runs on a free tier.

Tech Stack

Frontend

React

Hosting & Functions

Netlify

Automation

GitHub Actions

Data

Open States API 5calls API US Census Geocoder Nominatim

Extraction

LLM-based scraping (Gemini/Groq/OVH)