Tested live on 31 Oct 2026: RELAY COMPLETE · 2 communities · 2 helpers · 1 runner, copied from the overlay, not estimated. Leg 1 on antivex_dev reached 11 of 15 from real chat; we passed the baton to imastrom by hand from the panel; there a runner from leg 1 counted double and leg 2 finished 15 of 15 in 2 min 23 s.

Inspiration

A co-op quest that travels across Twitch. Each community plays one leg in its own chat. A raid passes the baton to the next channel. Raiders who keep chatting there become runners, and their messages push the baton twice as hard.

A raid usually ends the moment it lands: the viewers arrive, say hello, and nothing they did in the first channel carries over. We wanted the raid itself to be a game move, so a chain of small streams can play one shared quest.

What it does

On one channel — the quest

  • The streamer opens a local broadcaster panel, sets a goal (for example 20 steps in 10 minutes) and presses Start.
  • Every chat message is one step. A per-viewer cooldown stops spam, so twenty different people beat one person typing twenty times.
  • An OBS Browser Source overlay shows the relay card: the track with the baton, 14 / 20, the countdown and who is helping. When the goal is reached it shows LEG COMPLETE exactly once.
  • Pause, resume and reset at any time; the countdown pauses too. If Twitch drops the connection, the overlay says Reconnecting to Twitch… instead of freezing.

Across channels — the relay

  • Several channels connect to the same Quest Relay server. That is the network.
  • A relay has three legs by default and up to five (configurable). Each leg is played in one channel, with its own goal and time limit.
  • When a leg is done, the streamer raids another channel in the network. The raid passes the baton: the next leg starts there, and its overlay shows BATON INCOMING from @antivex_dev · 12 raiders · leg 2 of 3.
  • Viewers who helped in an earlier leg on another channel are runners: in the new channel each of their messages counts 2, with a cyan +2 RUNNER chip.
  • Channels waiting their turn see the baton coming: their overlay shows Baton is with @antivex_dev · leg 1 of 2 · 14/20, so the next community knows how close it is.
  • If the raid goes to a channel outside the network, the overlay says the baton was handed on and the relay ends there.
  • Fallback: if raid events are unavailable, the panel has a manual Pass baton to…, shown on screen as "Baton passed", never as a raid.
  • An offline simulation lets anyone try it without a Twitch account; it is always labelled OFFLINE SIMULATION.

Why it matters

  • Raided viewers get a reason to stay and talk in the new channel: their messages count double there.
  • Small streamers get a reason to raid each other and plan raid trains, because the raid moves a game they play together.
  • It still works as a one-leg quest on a single channel.

How we built it

  • Node.js, no runtime dependencies. A deterministic quest engine and a pure relay module (legs, runners, raids, duplicate-raid filter) are fully separated from Twitch and tested with a fake clock.
  • Twitch EventSub over WebSocket, one connection per channel, with channel.chat.message and channel.raid (to and from the channel). OAuth Authorization Code with a single scope, user:read:chat; raids need no extra scope. Tokens stay in memory on the streamer's machine.
  • In the live relay of 31 Oct the raid subscriptions were active on both channels, but leg 1 did not reach its goal in time, so the baton went to the second channel through the panel's manual Pass baton, not through a raid. The raid hand-off is shown in the offline simulation, which runs the same server code.
  • The server listens only on 127.0.0.1; overlay and panel get state through Server-Sent Events. The overlay never shows message text, only display names.
  • 67 automated tests cover the engine, the relay, the EventSub client (welcome, keepalive, reconnect, revocation), OAuth and the panel endpoints.

How we used Codex

  • Codex built the core. OpenAI Codex wrote the quest engine, the EventSub chat adapter and WebSocket client (welcome, keepalive, reconnect, revocation), the loopback OAuth server, the SSE stream, the broadcaster panel, the first OBS overlay and the offline simulation. Build plan steps 1–7 ran in the primary session 01a18184-d797-78c0-9414-7532ddd71291; scheduled Codex runs built the engine milestone and the chat adapter before it.
  • A written plan, one step at a time. Codex followed a build plan (docs/BUILD_PLAN.md). Each step: code, tests, npm test, then a commit with its exit codes reported before moving on.
  • Two agents, clear roles. Claude Code (Anthropic) wrote the plan and the relay design, and reviewed every Codex milestone with its own probes. The owner then assigned the relay layer (legs, baton hand-off, runners: 8c99efa, 3f41b86), the show-time overlay effects (9137c86), the last fixes (9f6738e) and, after the live run, the overlay redesign (a8e8c56) and the pre-submission fixes (97973de) to Claude Code; those commits carry its co-author trailer.
  • Review in both directions. Claude's review found that an invalid resume timestamp threw a TypeError the adapter did not catch, so it could crash during a stream; it sent a patch, and a scheduled Codex run checked it and integrated the fix and its test (5f41a66, 1a4d8f1). After live test B, Codex reviewed the test log, found two defects ("1 runners" in the panel, and a raw invalid-oauth-state error when a Twitch sign-in link had expired) and wrote the acceptance criteria for the fix (9f6738e, with tests).

Challenges we ran into

  • One raid, two events. With both channels on one server, the same raid arrives from the sender's and the receiver's subscription with different message ids. The relay accepts the first event for a channel pair within 60 s and ignores the second.
  • Making live input deterministic. Chat arrives late, twice or out of order. Everything is keyed on Twitch message ids and timestamps, and the engine is a pure function of events, so tests replay exactly.
  • Connection lifecycle. EventSub sessions must subscribe within seconds of the welcome, survive keepalive gaps and hand over to a new URL without subscribing again.
  • When raid events don't come. A refused channel.raid subscription must not take chat down with it. Raid subscriptions are optional: if Twitch refuses one, chat stays connected and the panel says "raid events unavailable — use Pass baton" (3f41b86). The manual pass is also what we used live on 31 Oct, when leg 1 ran out of time.
  • Goals for small streams. In the live relay of 31 Oct, with two people chatting, leg 1 stopped at 11 of 15 in 10 minutes. The goal and the leg time have to fit the size of the chat, so the panel lets the streamer set both before each relay.
  • Today's limit, stated plainly. Every channel in a relay joins the same local server, so the streamers have to share one machine. A hosted network is the first item in "What's next".

Accomplishments that we're proud of

  • It works on real Twitch chat: on 31 Oct 2026 a two-leg relay ran across two real channels, antivex_dev and imastrom, and ended RELAY COMPLETE · 2 communities · 2 helpers · 1 runner.
  • A viewer who helped in leg 1 became a runner on the second channel, and the overlay counted their messages double (+2 RUNNER). The baton was passed manually from the panel, and the video labels it that way.
  • Zero paid services, zero runtime dependencies, one OAuth scope.
  • 67 tests, all green on a clean clone of 97973de.

What we learned

  • Treating a Twitch integration as "events in, state out" makes it testable without a network.
  • Small, verified steps with a reviewer in the loop catch crash bugs before they reach a live stream.
  • An outside look shows what we stopped seeing. Blucca, another builder in this hackathon, watched the opening minutes of our live run and noted that a first-time viewer needs the next action right next to the counter. After the live run we redesigned the overlay with those suggestions (a8e8c56): "Chat to push the baton · reach 15 to pass it" next to the counter, "Next: @channel" before the hand-off, and a louder RUNNER ×2 chip. The live footage in the video shows the old overlay; the offline simulation shows the new one.

What's next for Quest Relay Overlay

  • A hosted network instead of one local server, so streamers on different PCs can join the same relay.
  • A route map on the overlay, with every community that carried the baton and a star for each completed leg. It is designed (docs/RELAY_DESIGN.md) but not built yet: today each overlay shows the current leg and who holds the baton.
  • Relay invitations, so a streamer can add the next channel without sharing a machine.
  • More quest types per leg: keyword quests, team splits.
  • Saved relay presets and a history of past relays.

Testing instructions for judges

  1. Node.js 22.16 or newer (we ran the tests on 24.19). Clone the repository: https://github.com/antivex/quest-relay-overlay (private, shared with [email protected]).
  2. npm test — runs all tests offline. (Verified on a clean clone with Node 24.19: all tests pass, 0 failed.) 2b. The server binds only 127.0.0.1:3000. If something else on your machine already uses port 3000, stop it first, or start the commands below with PORT=3100 and use that port in the URLs. For live mode (step 4) the Twitch redirect URI has to point at the same port as the server, so port 3000 is the simplest choice.
  3. No Twitch account needed: npm run simulate:live, then open http://127.0.0.1:3000/overlay?channel=demo_alpha and http://127.0.0.1:3000/overlay?channel=demo_beta in two windows. A scripted session with two fake channels, chat and a raid, labelled OFFLINE SIMULATION.
  4. Live mode: create a Twitch app with redirect http://localhost:3000/auth/twitch/callback, put its id and secret in .env (see .env.example), run npm start on port 3000, open http://127.0.0.1:3000/panel, connect one or more channels, start a relay and type in the chat.

Built With

  • claude-code
  • node.js
  • obs-studio
  • openai-codex
  • server-sent-events
  • twitch-eventsub
  • websocket
Share this project:

Updates

Submission history