-
-
Dashboard home page screenshot, 4 real stat cards, AI search bar, and activity feed. Proves real data flows from Twitch
-
n8n editor Full node chain: Webhook → HMAC verify → Get Viewer → Get Channel → Store Message → Should Reply? → WF-05..
-
AI stream search in action — Type "What did viewers talk about today?" and show the Gemini response with chat message citations
-
Viewer Facts page: AI-extracted, categorised, and confidence-scored facts per viewer. Requires triggering WF-08 first (run a stream, end it)
-
Twitch connection flow: TwitchConnectionCard → connect → EventSub status panel showing subscriptions registered. Demonstrates the OAuth
-
Architecture diagram — A simple 4-service data flow: Twitch → Next.js → n8n → Gemini → Python bot → Twitch chat.
Inspiration
I was sitting in a Twitch chat, watching messages scroll by faster than anyone could read them. The moment a message left the visible window, it was gone. No one was ever going to look at it again.
Thousands of messages where people share where they're from, what games they love, and what made them laugh just vanish into nothing every single stream.
That felt like a huge waste. All of that context and all of those little moments of connection were just lost. I started thinking about what would happen if chat wasn't ephemeral. What if instead of disappearing, those messages were actually being read and remembered? Not by the streamer because they have a game to play, but by a system smart enough to use that memory the next time the same person showed up?
That's exactly what Wrenone is. It's a way to make chat matter beyond the moment it appears on screen.
What it does
Wrenone is an AI co-host for Twitch streamers that builds a persistent memory of every person who chats. It learns their interests, gaming habits, personality, and history, and then uses that to reply in chat in a way that feels human and personal.
During a stream Every chat message is captured in real time using Twitch's EventSub API and stored in a database scoped to the creator's channel. When a viewer sends a message, the bot retrieves everything it knows about them and uses Google Gemini to generate a reply grounded in that context.
The creator sets a reply frequency ( f \in [0,\ 100] ). The bot then replies when a random draw ( r \sim \mathcal{U}(0,1) ) satisfies ( r \leq \frac{f}{100} ), which gives a smooth probabilistic control without hard scheduling.
After a stream ends An automated memory extraction pipeline runs over the session's full chat history. For each unique viewer, Google Gemini reads their messages and extracts structured long-term facts across five categories: interests, gaming, personality, life, and tech. These facts are stored permanently and recalled automatically the next time that viewer appears in chat.
The creator dashboard The dashboard provides real-time stats for messages processed, unique chatters, total viewer memories, and viewers with profiles. It includes a viewer fact library to browse every extracted fact, complete with categories and confidence scores. There is also an AI stream intelligence feature where creators can ask natural-language questions about their chat history, like "What were today's highlights?" or "Did anyone mention audio issues?"
The bot remembers across streams. A viewer who chatted three weeks ago is greeted with context instead of being treated like a stranger.
How we built it
Wrenone is built using four independently deployed services that pass data in a single direction.
Twitch EventSub (cloud) │ HMAC-signed POST ▼ Next.js / Vercel ← webhook gateway + creator dashboard │ async fire-and-forget ▼ n8n / GCP VM ← orchestration + AI workflows │ POST /send_reply ▼ Python bot / GCP VM ← Twitch IRC connection │ ↕ read / write Supabase (PostgreSQL 17) ← messages, viewers, viewer_facts Layer 1: Event ingestion (Next.js on Vercel) Twitch delivers channel chat and stream offline events to a Next.js webhook endpoint. Before anything else, I verify the HMAC-SHA256 signature that Twitch uses to sign every request:
$$\text{sig} = \text{HMAC\text{-}SHA256}\bigl(k,\ m_{\text{id}}\ |\ t\ |\ b\bigr)$$
where ( k ) is the shared secret, ( m_{\text{id}} ) is the Twitch message ID, ( t ) is the timestamp, and ( b ) is the raw request body. I use timingSafeEqual to prevent timing attacks.
The endpoint responds 200 OK immediately because Twitch requires a response within ~50ms. It then forwards the raw payload to n8n asynchronously.
Layer 2: Message processing (n8n WF-03 + WF-05) WF-03 re-verifies the HMAC across the service boundary, upserts the viewer record, stores the message, and applies the reply frequency gate. When the gate passes, it calls WF-05 as a sub-workflow.
WF-05 fetches the viewer's last 5 messages and all known facts, builds a context-rich prompt, calls Google Gemini (gemini-3.1-flash-lite via n8n's LangChain integration), and POSTs the reply to the Python bot.
Layer 3: Memory extraction (n8n WF-06 + WF-08) When the stream offline event fires, WF-08 loads up to 200 session messages, groups them by unique viewer, and asks Gemini to extract structured facts as strict JSON. Results are upserted into Supabase, so existing categories are updated and new ones are added. Facts accumulate across every stream.
Layer 4: Chat delivery (Python + TwitchIO) A FastAPI server runs alongside a TwitchIO IRC connection. The bot polls Supabase every 15 seconds to discover active channels and dynamically joins or leaves them. IRC rate limiting is handled with 5-retry exponential backoff and a 30-second global pause on msg_ratelimit responses.
Security Supabase Row Level Security is enabled on all 6 tables. Every dashboard API route passes through requireAuthAndChannel(), a guard that resolves the authenticated user's channel ID and injects it into every query. This makes cross-user data leakage impossible at the database level.
Challenges we ran into
Twitch's 50ms timeout vs. multi-second AI processing Twitch retries delivery if no HTTP response arrives in about 50ms. Generating an AI reply requires Supabase queries plus a Gemini API call, which takes easily 2 to 5 seconds. The fix was to have Next.js respond immediately and forward the data to n8n via a fire-and-forget fetch(). This fully decoupled the AI processing from Twitch's timing requirement.
Double HMAC verification across a service boundary Because Next.js forwards the raw payload rather than parsed JSON, both services must independently compute the same HMAC using the exact same three inputs:
$$\text{HMAC\text{-}SHA256}\bigl(k,\ m_{\text{id}}\ |\ t\ |\ b\bigr)$$
Getting the byte-exact message construction consistent across a TypeScript gateway and an n8n Crypto node required careful debugging on both sides, especially when dealing with forwarded headers and an unmodified body.
The channel:bot scope Registering a chat EventSub subscription silently requires the channel:bot OAuth scope. Twitch added this requirement in 2024 but didn't prominently document it. Anyone who connected before granting this scope would receive no chat events and no error message. I had to build a repair flow and an in-dashboard diagnostic hint to fix this for existing users.
Preventing bot reply loops Without filtering, every AI reply sent to Twitch chat would come back via EventSub, get processed by n8n, and trigger another reply, creating an infinite loop. I solved this using two independent layers. First, the Next.js gateway drops any message where chatter_user_login === BOT_NICK. Second, the frequency gate in WF-03 adds a probabilistic barrier. Both are needed since either one alone is a single point of failure.
Schema migration with live downstream consumers reply_frequency started as a text enum (always / every_5 / manual) and needed to become a 0-100 integer for granular control. The migration used a SQL CASE expression to convert existing rows:
sql
ALTER TABLE channels ALTER COLUMN reply_frequency TYPE integer USING ( CASE reply_frequency WHEN 'always' THEN 100 WHEN 'manual' THEN 0 WHEN 'every_5' THEN 25 ELSE 25 END ); Sequencing this safely while the Next.js API, the Python bot, and n8n workflows all continued running required really careful ordering of the migration and downstream updates.
Distributed token refresh across two independent services Both the Python bot and the Next.js app can independently detect an expired Twitch OAuth token and trigger a refresh. If both fire simultaneously, one overwrites the other's freshly written token. I mitigated this by making the bot proactively refresh at 15 minutes before expiry, well before Next.js would encounter an expired token during a request. A proper database-level lock is still on the roadmap.
Accomplishments that we're proud of
Building a fully working system, not just a prototype I am incredibly proud that this is a real, live system. It has actual Twitch OAuth, real EventSub subscriptions, and live Gemini AI calls running in production. It processes real events from real viewers, proving the architecture works end-to-end.
A genuine end-to-end event pipeline running on real data Real Twitch chat messages flow through EventSub, Next.js, n8n, Gemini, the Python bot, and finally to Twitch IRC. This isn't a prototype with mocked data. The system has processed real messages from real viewers in production.
Double HMAC verification across a service boundary Most webhook integrations verify a signature once. Wrenone verifies it twice. First in Next.js and then independently in n8n using the forwarded raw headers and unmodified body. The chain of custody from Twitch's signature to the AI layer is tamper-proof.
Automatic post-stream memory extraction When a stream ends, no manual intervention is needed. The offline event triggers the full extraction pipeline automatically. The next stream begins with a richer knowledge base than the last.
A four-service architecture with clean boundaries Next.js handles auth and webhook ingestion. n8n handles orchestration and AI. Python handles IRC. Supabase handles persistence. They are loosely coupled over HTTP, meaning any one can be replaced, scaled, or debugged independently.
Row-level security on every table A bug in one creator's session cannot expose another creator's data because this is enforced at the database level by Supabase RLS, not just at the application level.
What we learned
Twitch's EventSub is more complex than it looks. The scope requirements, challenge-response verification, retry semantics, and delivery timing constraints all interact in non-obvious ways. Getting a reliable ingestion layer took significantly more effort than a basic webhook integration.
LLM prompt engineering for two very different tasks requires two very different approaches. Reply generation is open-ended and persona-driven. Fact extraction needs strict JSON output, explicit deduplication instructions like telling the model to not repeat facts already in the knowledge base, and hard category constraints. If you don't do this, the model returns inconsistent, unparseable results.
Distributed systems surface race conditions that local development never shows. The token refresh collision, the in-memory deduplication cache resetting on Vercel cold starts, and the fire-and-forget forward silently dropping n8n errors are all things that just aren't visible when everything runs on one machine.
n8n is a serious orchestration layer, not just a no-code toy. Using n8n's LangChain integration and sub-workflow execution to chain WF-03 to WF-05 and WF-06 to WF-08 was much more capable than expected. The visual node editor also made it far easier to debug AI prompt failures since I could inspect the exact input and output of every Gemini call without writing logging code.
Supabase RLS requires thinking at the query level, not the application level. Writing RLS policies that correctly scope viewer facts to a creator's own channel via a join while still allowing the service-role key to write from n8n took several iterations. The three-key model of anon, authenticated, and service_role is fundamental to getting the security architecture right.
Paddle billing integration is more involved than adding a checkout button. IP allowlisting, webhook signature verification, linking Paddle customers to Supabase user IDs via custom metadata, and correctly handling subscription state transitions all need to work together without a seam.
What's next for Wrenone
Immediate AI stream intelligence: fully enable natural-language chat search for all creators using the latest Gemini Flash models Real usage tracking: wire the dashboard usage meter to live database counts instead of static values Data export: implement the memory download endpoint so creators can take their viewer data with them Short-term (next 30 days) Add a UNIQUE constraint on the twitch message ID for deduplication that survives serverless restarts Add TLS to the n8n instance using an nginx reverse proxy and Let's Encrypt Viewer opt-out: build a public page where any Twitch viewer can request deletion of their stored facts by username Surface bot moderator status in the dashboard because mod status determines whether the IRC rate limit is 100 or 20 messages per 30 seconds Medium-term Real-time dashboard using Supabase Realtime so stat cards update live during a stream instead of on page load Weighted memory: facts from more recent streams carry higher weight in the reply prompt, and old facts decay gracefully rather than accumulating forever Engagement analytics: track which viewers return most, what topics drive the longest reply threads, and which facts are referenced most in AI replies Long-term Expand beyond Twitch to YouTube Live and Kick. The architecture is platform-agnostic, so n8n workflows can just be duplicated with a different EventSub source Co-stream shared memory: if two creators share a stream, viewers known to one are automatically known to both Studio API: a public REST API so creators can build their own tools on top of the viewer memory graph
Built With
- fastapi
- framer-motion
- google-cloud
- google-gemini
- lucide-react
- n8n
- next.js
- paddle
- pm2
- postgresql
- python
- react
- supabase
- tailwind-css
- twitch-eventsub-api
- twitch-helix-api
- twitch-oauth-2.0
- twitchio
- typescript
- uvicorn
- vercel
Log in or sign up for Devpost to join the conversation.