Inspiration

Every team at a Twitch hackathon builds "AI replies to chat" or "chat controls the game." Both keep the audience as a wall of text. Chat is already a collective character — we wanted to give it a face, a mood and a voice, and let the streamer argue with their own audience.

What it does

Acclamation listens to a rolling window of real Twitch chat over EventSub. When the streamer asks a question, it clusters the room into stances, measures the share of unique chatters behind each one, and answers as a single voice — with 2–5 receipts quoting real usernames and messages. When chat is split, the glyph face fractures into two and two voices argue with each other. A Streamer-vs-Chat scoreboard and pinned channel lore persist across streams.

How we built it

  • Twitch: EventSub WebSocket v1 (channel.chat.message, channel.chat.message_delete, channel.raid, channel.subscribe), Helix for subscriptions, authorization-code OAuth with user:read:chat and channel:read:subscriptions.
  • AI: any OpenAI-compatible chat-completions endpoint; JSON-object mode when supported, strict local validation always.
  • Frontend: canvas glyph swarm (one glyph per message) that samples a drawn face mask; pure HTML/CSS/JS, no build step, transparent for OBS.
  • Runtime: Node 22+, zero dependencies. node:sqlite for lore, calls and the scoreboard, with an in-memory fallback.
  • Offline harness: a fake EventSub WebSocket server (RFC 6455 handshake, welcome/keepalive/reconnect/notification/revocation) so the whole product could be built and tested without a Twitch account or network.

Challenges we ran into

  • Making consensus falsifiable. Lexical clustering over-splits short chat; prototypes and stemming fixed most of it. The confidence shrinks honestly on small samples instead of overclaiming.
  • Grounding. Letting the model write quotes is how every similar project eventually quotes a deleted message. We made the model cite ids, and resolved text locally.
  • Deleted messages. The scrub has to happen on the window, before clustering, and ids need tombstones — tested end to end.
  • Demo latency. The face starts forming on trigger:start, before the model call, so the audience never watches a spinner.

Accomplishments that we're proud of

  • A split-mode moment that is genuinely new on a Twitch stream: one chat, two voices, one face breaking in half.
  • 66 tests covering clustering, ingestion filters, the JSON contract, the EventSub state machine (including reconnect and keepalive watchdog) and the full replay pipeline.
  • A live-leg doctor that refuses to say "working" until a real chat message arrives over the socket.

What we learned

  • Consensus is easy to fake and hard to measure. The first clusterer used IDF-weighted centroids; as a cluster filled up, similarity to its own centroid diluted and nine people making the same joke split into seven "stances". Stemmed token matching against cluster prototypes fixed most of it, and shrinking confidence toward 50% on small samples made the numbers smaller — and true. A demo that reports 43/29 is more convincing than one that reports 96%.
  • Grounding is an architecture decision, not a prompt line. "Never invent quotes" eventually fails if the model writes the quote. Making the model cite message ids and resolving the username and text locally turned the promise into a validator: invented ids are rejected, retried once, then replaced by the local voice. The audience never sees model-authored attribution.
  • Deletion is a systems problem. A deleted message must be removed from the window, tombstoned so it cannot be re-added, excluded before clustering, absent from the prompt, and impossible to resolve as a receipt — with a test asserting the deleted fixture id never appears in an answer. Any one of those layers alone is a bug waiting for the worst possible moment.
  • Build the network boundary first. The fake EventSub server (handshake, welcome, keepalive, reconnect, revocation) meant the overlay, clustering and dashboard never waited on Twitch credentials, and the reconnect state machine was tested in milliseconds instead of during a stream. The harness is why the live leg is a 20-minute setup instead of a day of blocked work.
  • Refusal is a feature. Under four chatters Acclamation says "that is not a crowd yet" and never calls the model. It started as a safety rule; in rehearsal it became the most trustworthy beat in the demo — the one moment the audience believes the numbers.
  • Latency is composition. The face starts forming on the trigger, before the model call finishes. If the audience ever sees a spinner, the product feels like an API instead of a character — so the animation is part of the architecture, not polish.

What's next for ACCLAMATION

Embedding-based clustering; channel.ban for instant timeout scrubbing; per-voice TTS staging in split mode; a Stream Deck action; cross-stream lore so chat's calls follow the channel.

Built With

Share this project:

Updates

Submission history