How We Forecast Every State Legislative Seat: Inside the 99 Chambers Data Pipeline
A first-hand look at how 99 Chambers forecasts about 6,100 state legislative seats plus Senate, House and governor races every night: scraping, Postgres/PostGIS, the simulation model, a state rules registry and publishing to Cloudflare D1.
By Abdul Hadi Bharara, Founder, Exprtec
Published
Most election forecasts stop at the U.S. Senate, the House and governors. That is a few hundred races. State legislatures are where the volume is: in 2026 there are 89 chambers with regular elections across the 50 states and DC, and about 6,100 seats on the ballot. Each one has its own district map, its own candidate list, and often its own rules about how the winner is decided.
We built 99 Chambers to forecast all of them, district by district, alongside every Senate, House and governor race. It refreshes itself every night without anyone touching it. This article walks through how the pipeline works, from the first scrape to the page a reader sees, and what was hard.
The Shape of the System
99 Chambers is three independent apps that share no code. A Python pipeline scrapes, stores and models the data in Postgres with PostGIS. A small internal admin panel lets us inspect runs, edit candidates and queue jobs. The public site is an Astro app on Cloudflare Workers that reads a Cloudflare D1 database. The only contract between the pipeline and the site is a set of JSON documents, one per page, typed on both sides.
Jobs run from a durable queue in Postgres. A runner polls a jobs table (queued, running, succeeded or failed) and enqueues cron schedules. The nightly sequence is simple: refresh inputs at 06:00 UTC, run the forecast at 07:00 UTC, publish the site at 08:00 UTC.
Where the Data Comes From
No single source covers every state legislative race, so the pipeline stitches many together. Each loader records its provenance (source, source URL and fetch time) and keeps the raw file, so any number on the site can be traced back to where it came from.
Races and candidates
Wikipedia race pages for federal, statewide and legislative contests, plus official general-election candidate lists from state election offices where Wikipedia has none.
Current legislators
The Open States bulk CSV (CC0) gives the incumbent party of every seat, chamber composition and the holdover seats not on the ballot.
Polls
The New York Times polls database for Senate, governor, House and generic ballot; Wikipedia poll tables for approval, other statewide offices and the rare state legislative poll.
Money
FEC bulk files for federal candidates, New York's Board of Elections datasets on data.ny.gov, and 35 state campaign-finance loaders that read each state's disclosure site.
Geography and history
Presidential results from precinct shapes area-weighted onto the 2026 district plans, ACS demographics, religion census data, and past legislative results for training.
Every scraper goes through one polite HTTP client: an identifiable user agent, robots.txt respected, a rate limit and retries. Sources that change rarely, such as past results and district boundaries, are loaded once and frozen. Sources that change daily, such as polls, are refreshed nightly, and each source is refreshed only as often as it actually changes.
How the Model Works
The forecast publishes two versions. Horizon, the headline, combines polls, fundamentals and a small amount of CANTOR, our method for borrowing information from similar races, and is nudged toward a historical prior so it reads as an Election Day forecast. Pulse uses polls and CANTOR only, at today's environment, with wider uncertainty. Neither uses expert ratings.
- Polling averages weight each poll by recency, sample size, pollster rating and how many polls that pollster has released, then adjust for house effects and likely-voter versus registered-voter samples.
- Partisan lean comes from recent presidential results in each district, measured against the nation. A national environment comes from the generic ballot blended with what Senate and House polls imply.
- Fundamentals are regressions trained on past elections: lean, incumbency, the seat's last result, fundraising and candidate experience, fit separately for incumbent and open races.
- State legislative races are almost never polled, so CANTOR carries the residuals of polled races in the same state to similar districts, matched on lean, demographics, religion and geography.
- State offices run in a state environment estimated from that state's polled governor, statewide and legislative races, shrunk toward the national environment.
The model then runs 40,000 simulations. Errors are layered: a national swing, a draw per region, per state and per legislative chamber, a correlated demographic swing, and local noise per race. Because every race in a simulation shares the same national and state draws, chamber totals, supermajorities and trifectas come out correlated the way real elections are, rather than as independent coin flips.
A Registry So the Code Never Says "If Alaska"
The hardest design problem was not statistics. It was the 51 different rulebooks. Alaska uses a top-four primary and ranked-choice general elections. Georgia sends races to a runoff when nobody clears 50%. New York lets candidates run on several party lines. New Hampshire elects a 400-member House from multi-member and floterial districts. Virginia, New Jersey, Louisiana and Mississippi elect their legislatures in odd years.
Every one of those facts lives in a single place: a registry with one Python package per state plus DC. Each defines its chambers (seats, terms, seats up in 2026, supermajority thresholds, tie-breakers), its primary type, runoff rule, ranked-choice offices, fusion, multi-member districts, and plain-language explanations of each quirk. Loaders, the model and the publisher read the registry and contain no state-specific branches.
The payoff is that changing a state's rules is an edit to one file followed by a rerun. When Louisiana moved its 2026 U.S. House races to a November all-party primary with a December runoff, that was a registry change, not a code change. The registry also keeps a record of earlier cycles' rules, so the 2022 backtest replays 2022 as it was.
Publishing: One Row Per Page
The publish job builds one JSON document per page, plus district polygons for the address lookup. It hashes each row and writes only the rows that changed to D1, then bumps a data version. The site's HTML cache keys include that version, so there is no cache purge step: the next request after a publish simply renders against new data. Most page views cost no database reads at all.
What Was Hard
- Identity. The same person appears as an FEC id, an Open States id, a Wikipedia title and a name typed differently on a county list. Every identifier resolves through one people table, and names are matched only the first time an identifier is seen.
- Party labels. Every source spells parties its own way. Labels resolve through one lookup table in Postgres, and loaders are not allowed to keep their own mapping.
- Edge cases that look like bugs on the page. When Alaska's top-four put several finalists from one party on the November ballot, the site initially showed the best-funded one as that party's representative, not the one leading in the polls. We changed the rule so the poll leader represents the party, which in turn broke the incumbent marker, so incumbents are now marked by name rather than by party.
What This Means for Your Team
If you run a newsroom data desk, a campaign analytics shop or a civic-data project, the pattern transfers: put every jurisdiction-specific rule in data rather than code, record where every number came from, freeze what does not change, and publish only what did. The same pattern works outside elections, for any business that pulls from dozens of inconsistent sources and needs a dependable result every morning.
We designed and built 99 Chambers end to end at Exprtec. If you need election data, or any multi-source data pipeline, scraped, modelled and published automatically, book a call and we will walk through what it would take.
Book a call with ExprtecSources
- 99 Chambers: 2026 election forecasts, 99 Chambers
- State legislatures forecast 2026, 99 Chambers
About the author
Abdul Hadi BhararaFounder, Exprtec
Hadi founded Exprtec and builds its automation systems end to end: the AutoResponder that answers our own inbound email and chat, the LeadsLLM negotiation agent, PawPrint AI, LedgerLens, ListingSight, and the 99 Chambers election forecast. He writes about what those builds taught us, including what broke.
More from Hadi