This is a submission for Weekend Challenge: Passion Edition
What I Built
Pollux is an anonymous, real-time tracker of Nigerian political sentiment. Pick any of 52 politicians, national or state level, and vote support, undecided, or oppose. Leave a short comment saying why. Then watch the tally move live. See how the country breaks down across the six geopolitical zones covering all 36 states and the FCT, and read a neutral, AI-generated briefing on who the person is and what is being said about them right now.
Pollux was just the right answer to the passion angle poised in the challenge description. Nigerian politics does not do lukewarm. Online political discourse is loud and heated but this can mean that people’s intentions are hard to read. All you learn is who is shouting the loudest on Twitter. On top of that, the loudest voices have been directly or indirectly “dealt with” by offended politicians with the means to bend state institutions to their needs, so most people are reluctant to share their thoughts.
I took an idea and design by my friend Ope who spun up a first prototype with Claude, which is still live at pollux-ng.netlify.app. When he shared the project with me and gave me permission to take it over, this challenge became my reason to give his design the backbone it deserved: a real database, a server boundary, working AI, and honest numbers.
That last one mattered most to me. Pollux should capture the heat itself instead of the noise. No accounts, no follower counts, no badges, no clout scores. Just an anonymous vote and a short reason why, feeding a running tally you can watch move in real time. The goal isn’t a “who’s winning 2027” scoreboard, neither do the numbers translate to real election results.
Main features
- Vote support, undecided, or oppose on 52 national and state-level politicians, with a 60-second cooldown and the option to retract
- Anonymous 280-character comments, moderated by AI before anyone sees them
- A live leaderboard, plus a Pulse page showing the top movers today, this week, and this month
- Regional breakdowns across the six geopolitical zones, built from the state and zone each voter self-declares
- Per-politician detail pages with a 0 to 100 Passion Meter, dominant emotions, neutral supporter and critic digests, and a grounded factual briefing
- A methodology panel on every page that reads from the same constants the code enforces
- Shareable politician cards via the Web Share API, with a clipboard fallback
Demo
Live app: pollux-ng.vercel.app
The best things to watch for: the tally moving without a refresh, and a detail page with the Passion Meter and AI briefing open.
Code
akcumeh
/
poll-ux
Poll – ux is an anonymous, real-time platform for tracking Nigerian political sentiment. Users can vote, comment, and explore public opinion on politicians across all 36 states, with live rankings, regional insights, and transparent, manipulation-resistant data.
Pollux
Anonymous, real time tracker of Nigerian political sentiment. Vote support, undecided, or oppose on politicians across all 36 states, comment, and explore live rankings and regional insight. Numbers below the minimum sample are withheld, never invented, and everything a machine wrote is labeled.
Architecture
A single Vercel deployment serves both halves:
-
src/is a vanilla TypeScript + Vite frontend. Pages render as template strings, event handlers attach towindow. No framework. -
api/is the backend: Vercel Node serverless functions (cast-vote,moderate-comment,ai-insights,briefing,report-comment). They hold the Supabase service role key and the Gemini key, enforce vote cooldowns and daily limits server side, and are the only thing that ever calls Gemini (gemini-2.5-flash, via@google/genai). - Supabase is Postgres plus realtime, nothing else.
supabase/migrationsholds the schema. The client only ever reads tables directly with the anon key; every write…
How I Built It
Where it started
The original was a static site where the browser held write access to everything. Votes and comments went straight from client code to Supabase with the anon key, which means anyone with devtools open could forge unlimited votes. Rate limiting was UI state in localStorage. The “Comment posted ✓” toast fired before the network call even ran.
The regional numbers were invented. A function multiplied national support by hardcoded per-politician bias coefficients, and when there were zero votes it returned 40 + Math.floor(Math.random() * 22). The “AI insights” button copied a prompt to your clipboard with a toast telling you to paste it into any AI tool of your choice. However, it is what a first prototype looks like, and the design was strong enough that I wanted to keep it.
The net diff by the end:
- 77+ files changed
- Roughly +7,400 / −4,300 lines
One deployment, two halves
I ported the deployment from Netlify to Vercel specifically to get serverless functions, and that split is now the backbone of the whole trust model. A single Vercel project serves everything. src/ is a vanilla TypeScript + Vite frontend with no framework at all. Pages render as template strings and event handlers attach to window. The app is mostly read-heavy dashboards, so the bundle stays tiny and I keep full control over what hits the network and when. The entire project has exactly two runtime dependencies: @supabase/supabase-js and @google/genai.
api/ is the half I’m proudest of: five Vercel Node serverless functions, cast-vote, moderate-comment, ai-insights, briefing, and report-comment.
- The browser only ever holds the Supabase anon key, and Row Level Security policies mean it can only read. The service-role key and the Gemini key never touch client code.
-
Every write, whether it’s a vote, a retraction, a comment, or a report, goes through
api/, which holds the service-role key. - Rate limits (a 60-second vote cooldown, 150 actions per day, 20 reports per hour) are enforced server-side, returned as proper HTTP 429s with
retryAfterSeconds, where nobody can tamper with them. - Input validation happens at the API too: uid length checks, known-politician-id validation, 280-character truncation, and direction whitelisting.
Postgres triggers automatically recount the vote aggregates on every insert, update, and delete, and Realtime subscriptions push fresh tallies to every open tab so that nobody needs to refresh to see the latest data. The schema itself is now versioned in supabase/migrations, which the original repo never had.
Trust by default
This part shaped almost every decision:
- Less fiction. AI and sentiment analysis do not happen until a certain threshold is reached: 10 votes per politician, 5 votes per region, 5 comments before any AI digest is generated. Similarly, real regional data is collected from voters’ voluntarily-declared location, and used to populate the zone aggregates.
-
One source of truth. The thresholds live in
src/lib/constants.ts, imported by both the frontend rendering and the API functions. - Server-side timestamps everywhere. The Pulse page’s top movers are counted from server-side vote timestamps, so the rankings are identical on every device. There is no “your view” versus “my view”.
-
Robust failure system. RLS restricts
poll_commentsreads tostatus = 'approved', so even if the moderation handler misbehaves (eg, I run out of API credits), held comments still can’t leak to other users. - Everything AI-generated is labeled. The briefing, the Passion Meter score, the dominant emotions, and both digests are each marked as AI-produced, with a timestamp so you know exactly how stale they are.
Google AI (Gemini) does three jobs
I was targeting the Best Use of Google AI category from the start, and that set a bar: replace the clipboard trick with AI that carries real weight in the product. I use gemini-2.5-flash via @google/genai, the only model in the stack, and it never runs in the browser. The GEMINI_API_KEY lives only in server-side env and is referenced exclusively by api/.
-
Comment moderation. Comments on Nigerian politics arrive in English, Nigerian Pidgin, Yoruba, Igbo, Hausa, or any mix of the five, full of slang and deliberate misspellings written to dodge filters. The moderation prompt is written for exactly that: it judges meaning, not keywords, and classifies each comment as
clean / abusive / spam / incitementusing structured JSON output with a strict response schema at temperature 0.2. Strong political disagreement is explicitly clean, and tribal or religious incitement is treated as the most serious label, because in the Nigerian context it is. The moderator fails pending: if Gemini is unreachable (eg, outage due to rate limit or billing problem), the comment stays visible only to its author and a background sweep classifies it the moment the API recovers. Unchecked content is never published, while also ensuring nobody’s comment silently disappears. - Debate insights. Once a politician has 5 approved comments, Gemini reads up to the 80 most recent, each tagged with the commenter’s declared stance, and produces a 0 to 100 temperature (how heated the debate is, not which side is winning), 2 to 4 dominant emotions, and two short neutral digests: what supporters mainly argue, and what critics mainly argue, at most 80 words each. It’s prompted to read sentiment the way a Nigerian reader would (“e no easy” can be praise, “God abeg” can be exhaustion) and to say plainly when a side is absent.
- Grounded briefings. Each politician gets a factual briefing of at most 130 words, generated with Google Search grounding, covering their current role, recent news, live controversies, and the debates around them. The prompt forbids praising, condemning, predicting election outcomes, and inventing approval numbers.
Each call is properly engineered: timeouts of 6, 15, and 20 seconds respectively, low temperature, and strict response schemas where the output feeds the UI. Each job also caches on its own terms. Insights are updated only every time the comment section changes. Briefings live in hourly buckets, with a refresh control that appears after a minute. If an insights regeneration fails, the last good version is served instead of an unresponsive UI.
The hardest parts
Cache invalidation lived up to its reputation. My git log for the stretch includes “fix: briefing now in hourly buckets”, “fix: rejected comments persisting”, and “fix: pending comment state + cooldown”. Every one of those was the cache and the realtime channel disagreeing about what was true, and each fix meant deciding which of them got to be right.
The other one was moderation. Handling five languages plus Nigerian slang was the design problem I sweated over the most, and ended up letting Gemini make the judgment calls.
I also reorganized the detail page so the comment thread sits above the regional panel, since the comments are where the votes get context.
Connection to the Theme
The theme is Passion, and it shaped what I built more directly than any feature list suggests. The Passion Meter exists because of it: a gauge on every detail page that doesn’t tell you who is winning, only how much heat a debate is carrying, whether that heat comes from supporters or critics. The AI moderation and the debate digests came from the same place, because measuring passion honestly means keeping the thread readable and summarizing both sides without picking one.
Nigerians are among the most politically passionate people anywhere. We argue about governors in barbershops and every election season is a national obsession. A side project that captures that intensity without the bot armies was a fun one.
What’s Next
The anonymous model is a feature (low friction means more honest votes), but it means I lean entirely on rate limits and moderation to keep things sane. After the challenge, I want to explore:
- more sophisticated vote-integrity signals beyond cooldowns and daily ceilings
- a richer regional visualization, ideally an interactive map of the 36 states
- surfacing comment trends over time, beyond the current digest
- an admin review queue for held and reported comments, so moderation decisions are auditable
Prize Categories
Best Use of Google AI. Gemini 2.5 Flash runs three distinct, load-bearing jobs in Pollux:
- real-time comment moderation across five languages, with structured JSON classification that fails open
- debate analysis producing the Passion Meter score, dominant emotions, and neutral digests for both sides
- grounded factual briefings via Google Search grounding, all behind serverless functions with caching and graceful fallback
Author
Thank you for reading this far! Connect with me via:
답글 남기기