Tracks congressional bills through their full lifecycle — introduced, in committee, passed House/Senate, signed or vetoed — and pairs that with FEC itemized donor retention for House/Senate campaign committees across election cycles. Two ways to connect the two: a bill's sponsor links directly to their campaign committee's retention numbers, and a bill's policy area links (heuristically, via keyword matching) to industries with financial ties to that sponsor.
The whole stack is free: Congress.gov and OpenFEC both give free API keys, the crosswalk data is public, hosting is GitHub Actions + Streamlit Community Cloud, and storage is a single SQLite file. Nothing here requires a paid tier of anything.
- Ingestion (
agents/legislators_agent.py,agents/bills_agent.py,agents/fec_candidates_agent.py,agents/fec_contributions_agent.py) — pulls the bioguide↔FEC crosswalk from unitedstates/congress-legislators, bills/actions/subjects/cosponsors from Congress.gov, and candidates/ committees/itemized Schedule A contributions from OpenFEC. Runs entirely offline, on a schedule, never at request time. - Retention math (
agents/retention_agent.py) — for each campaign committee, matches donors between consecutive election cycles via a name+ZIP fingerprint (SQL self-join ondonor_fingerprints), and writes only the aggregate "Itemized Donor Retention" percentage todata/tracker.db. Individual contributor rows live only in the gitignored.cache/fec_raw.dband are never committed or displayed. - Industry signal (
agents/industry_link_agent.py) — keyword-matches the same cached contributor employer/occupation/PAC-name text againstindustry_keywords.py's policy-area table, producing a directional, approximate signal — not an authoritative industry classification. - Data-pairing layer (
sql/views.sql) —v_active_bills_overview,v_bill_sponsor_retention, andv_bill_industry_signalexpress every cross-table join the app needs, as plain SQL views. app.pyis the Streamlit UI. It makes zero network calls — it only reads the committeddata/tracker.db, so no API keys are ever needed to run or deploy the app itself.
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
streamlit run app.pydata/tracker.db ships with the repo (once you've run a refresh at least
once — see below), so the app works locally without any API keys. Keys are
only needed to run a refresh.
- Congress.gov — https://api.congress.gov/sign-up/ →
CONGRESS_API_KEY - OpenFEC — https://api.data.gov/signup/ →
FEC_API_KEY
cp .env.example .env # then fill in both keys
python scripts/refresh_all.py # populates data/tracker.db for the first timeNever commit .env — it's already in .gitignore.
- Push this repo to GitHub.
- Add
CONGRESS_API_KEYandFEC_API_KEYas GitHub repo secrets (Settings → Secrets and variables → Actions) — these are used only by the scheduled workflows, never by the deployed app. - Go to https://share.streamlit.io, connect the repo, set the main file to
app.py. No secrets need to be set in Streamlit Cloud — the app only reads the committed database. .github/workflows/refresh_bills.ymlruns daily;.github/workflows/refresh_fec.ymlruns weekly. Each commits an updateddata/tracker.db, which triggers Streamlit Cloud to auto-redeploy.
The first historical pull across ~535 committees is too large for a single
scheduled run. After deploying, trigger refresh_fec.yml manually a
handful of times via Actions → Refresh FEC data → Run workflow, raising
chunk_size if you want fewer, larger runs. Each run is resumable — it
picks up committee/cycle pairs that aren't yet complete in
fec_ingest_state. Once every committee shows complete, the weekly
schedule is enough to stay current.
Stated plainly rather than glossed over:
- "Itemized Donor Retention" only reflects itemized donors — FEC Schedule A only includes contributions that aggregate over $200 per election. Smaller donors are invisible to this metric by construction, not by a matching failure.
- Donor matching is a name+ZIP fingerprint, not a stable person ID. Nicknames, marriage-name changes, and moves cause undercounting; common names in dense ZIP codes and joint filers cause overcounting. This isn't fixable without a paid identity-resolution service.
- Industry/interest-group linking is keyword-based, against a small
hand-curated table (
industry_keywords.py), not an authoritative CRP/OpenSecrets industry classification. Treat it as a directional signal worth investigating further, not a definitive tie. - Very high-dollar campaigns' itemized contributions are capped at
config.FEC_MAX_ROWS_PER_COMMITTEE_CYCLEper committee-cycle, taken in chronological order — this only meaningfully affects the very largest committees' precision. - Bill stage is derived from keyword-matching action text
(
agents/bills_agent.py:derive_stage), not a first-party stage field — Congress.gov doesn't expose one directly.