1. What the alignment score is
An alignment score is a single percentage that tells you how often a representative votes the same way as the progressive majority on the policy issues you care about โ across all the roll-call votes on those issues that we have on record.
For each issue you select on /app, VoteClarity counts every roll call that has been tagged to that issue, then asks two questions of each one: which way did the Democratic majority vote? (the "progressive direction" for that vote) and did the rep vote that same way? A simple average across the issues you picked โ with non-voting and tie votes excluded โ becomes the % you see on a /rep profile.
The per-issue formula, in plain language:
count(aligned_yields_to_majority_dir) รท count(partisan_yields_on_tagged_issue) ร 100
Tie votes (a 50/50 split in the Democratic caucus) are excluded from both the numerator and denominator โ the same skip rule the /alerts flag detection uses. A rep whose party abstain in unison isn't "half-aligned" โ that vote simply doesn't carry a directional signal, so it doesn't move the score.
The overall score you see at the top of a /rep profile is the average across the 10 issues you picked (or fewer, if some issues had zero tagged votes on the floor for that rep). When nothing has been tagged yet, the page degrades to โ ("no data") rather than inventing a score out of thin air.
2. How a rep vote is mapped
For every roll call in the Senate and House, VoteClarity pulls the Yea/Nay position each member recorded, then determines which way the Democratic majority of that chamber voted on the same roll. Tally that against the rep's vote โ did they vote with the Democratic majority's direction, or against it?
That mapping boils down to three steps per vote:
- Step 1 โ Tally the Democratic majority: count Yea among D-party members and Nay among D-party members on this roll. Whichever side has more is the "progressive direction" for this vote. If it's a true tie (the D caucus split 50/50), this roll is skipped โ there is no progressive direction to compare against.
- Step 2 โ Record the rep's position: read the rep's recorded Yea/Nay on the same roll from
member_vote_positions. Absences and abstentions ("Present," "Not Voting") aren't counted into the alignment math. - Step 3 โ Match or miss: if the rep's position equals the progressive direction, that's a progressively-aligned vote (+1 for the numerator). Otherwise it's a miss (0). Both are added to the denominator โ every counted partisan roll call is included whether the rep voted with or against.
That same SQL is what powers the per-issue score circles on every /rep profile and the row-level rep cards on /districts; the methodology here is identical.
3. Issue weights and your weighted lean
When you take the quiz on /app or subscribe to the digest, you mark each issue on a 1โ5 scale and pick a direction: progressive or conservative. Those answers are stored as { slug, weight, lean } rows inside subscribers.issue_prefs (a JSONB column). The weight is a 0โ1 number โ the higher you rated it, the more a single rep miss will move your alert signal for that issue.
For each roll call, the alignment check for you (not the aggregate score) considers only the issues you actually picked. A representative's contribution to a weighted delta on one tagged issue is computed as:
โweight when the rep's vote opposes your lean, or 0 when it agrees.
If you marked healthcare at weight 0.8 and your rep votes against the progressive direction on a healthcare bill, the per-issue delta for that vote is โ0.8. If you marked tech & privacy at weight 0.2 and the same rep misses on that one, the delta is only โ0.2 โ the issue matters less to you, so the mismatch is smaller.
That's why two voters with different priorities can subscribe to the same rep and receive very different /alerts feeds โ same votes, different signal strength.
4. The score-delta threshold
VoteClarity only fires an /alerts flag when the weighted mismatch passes a minimum bar โ we don't want every tiny mismatch on a low-priority issue to fill your inbox. The relevant constant lives in the detector that decides whether a flag is created:
ALERT_DELTA_THRESHOLD = 0.40
In plain language: we only flag a vote if the weighted mismatch is at least 0.40 in absolute value. The detection rule, represented as a formula:
|delta| ≥ 0.40, where delta is โ|weight| on a mismatch and 0 on agreement.
What this means in practice:
- If you rated an issue at weight 0.9 and the rep misses it,
|delta| = 0.9» 0.40 » flag fires. - If you rated an issue at weight 0.5 and the rep misses it,
|delta| = 0.5» 0.40 » flag fires. - If you rated an issue at weight 0.3 and the rep misses it,
|delta| = 0.3< 0.40 » no flag โ the issue doesn't matter enough to you for this single miss to be worth your inbox's attention. - If the rep's vote agrees with your lean on that issue,
delta = 0» no flag โ agreement is never alerted.
The threshold lives in alerts/detector.js as the exported constant ALERT_DELTA_THRESHOLD = 0.4, and is passed explicitly to computeScoreDelta({...}) at each flag-evaluation call in alerts/service.js. There is no hidden second filter on top of it.
5. What fires a /alerts flag
All five of these conditions must hold for a single roll call to land in your inbox:
- You're subscribed with a real email and at least one issue weight. Anonymous browsers can score their reps on /app, but the alert inbox is subscriber-only. Subscription lives in the
subscriberstable, keyed by email. - The rep is on your watched list. Subscribing to a state or ZIP code implicitly watches your senators and house rep. Each watcher row in
vote_watchersis the join key. - A tagged roll call lands with the rep's Yea/Nay matching or opposing your lean. Tagging is done by keyword rules first, with an LLM fallback through Polsia's OpenAI proxy for votes that keywords miss. Untouched votes never produce alerts.
- The absolute weighted delta is at least 0.40. Same threshold as section 4 above, applied per issue: a mismatch with
|weight| < 0.40is suppressed. - The deploy has
ALERTS_ENABLED=true. This is the platform-level on/off switch. When the flag is disabled, flag generation is a no-op โ your score still computes correctly, but no new flags are written until the switch is flipped back on.
If you see a flag in your inbox, every one of those boxes has been ticked.
6. Where the data comes from
Everything VoteClarity scores comes from two primary sources:
- Roll-call votes from
api.congress.govfor both the Senate and the House. On startup (server.js) we run a chunked full backfill for the current Congress, with progress committed toingestion_logafter each chunk so a deploy interruption loses at most ~50 rolls. - Vote โ issue tagging by a keyword-rule pass first, with an LLM fallback (through Polsia's OpenAI proxy) for votes keywords can't classify. The same tagging pipeline runs both during the startup ingest loop and as ongoing backfill.
The /rep profile SQL and the /alerts detector query the same member_vote_positions, roll_call_votes, and vote_issue_tags tables, so a number on your profile is computed from exactly the same source data as a flag in your inbox. There is no per-page re-summarization โ one pipeline, one explanation.
Browse voting records by issue
Each issue page shows the top 5 most-aligned and most-opposed active members of Congress for that category, with per-vote counts and links to their full voting profiles.