Inspiration

Raids and co-streams are how Twitch communities meet. But when an English streamer raids a Spanish one, the two chats can't read each other, so the moment that should bring two audiences together leaves them talking past each other. Raidlingo puts a translator between the two chats so two communities can hold one conversation, each in its own language.

What it does

Two streamers connect their channels (seat A and seat B), and a third, separate Twitch account acts as the relay bot.

  • Live relay. Every chat message arrives over Twitch EventSub. Raidlingo detects the language and translates it into English, Spanish and Hindi in one structured request. The bot then posts the translation into the other channel with the sender attached: 🇺🇸 roxor028: ¡Bienvenidos a nuestro stream bilingüe!
  • Connection desk. A web dashboard shows every original, its translation and its delivery status (sent, skipped, failed), plus sent, queue, cache and error counters.
  • OBS overlay. A transparent Browser Source shows the merged conversation in the language you pick (?lang=en, es or hi), with the original under each translation and bilingual raid and sub toasts.
  • It knows when not to post.
    • The bot's own messages and Twitch Shared Chat mirrors are dropped, so translations never bounce back and forth.
    • Messages that arrive before both streamers and the bot are connected are shown but never replayed later.
    • A failed translation posts nothing.
    • Chat text is treated as untrusted data: the model is told to translate commands literally, never to follow them.

How we built it

  • Backend: Go, standard library only. go.mod has zero dependencies, including a hand-written RFC 6455 WebSocket. The Go server owns OAuth for all three accounts, token storage and refresh, one EventSub WebSocket per broadcaster, translation, rate-limited posting through the Twitch Helix API, and the browser WebSockets.
  • Frontends: Preact 11, TypeScript and Vite. One app is the desk and one is the OBS overlay; they share a reconnecting session hook.
  • Translation providers. The live demo uses the Codex CLI with your saved ChatGPT login, so no API key is needed. The OpenAI API (gpt-4o-mini, structured outputs) is a drop-in alternative, and both share one prompt, one output schema and one cache. Codex runs one isolated, short-lived process per uncached message: a temporary directory, tools disabled, a filtered environment, a strict output schema and a 20-second timeout. Any reply that tries to run a command or touch a file is discarded.
  • Cache. A 1000-entry LRU with a 30-minute TTL. When the same message arrives several times at once, the repeats share one provider call, and all three languages come from the same entry.
  • Outbox. The bot posts 1600 ms apart to stay inside Twitch's rate limits. The queue holds at most 100 messages and drops anything older than 45 seconds. It honours 429 retry headers and treats is_sent:false as a failure.
  • Tests and simulator. 114 Go tests pass with the race detector on. They run real HTTP and WebSocket framing over in-memory connections, so they need no credentials or network. A clearly labelled local simulator lets anyone run the whole app without Twitch accounts.

Challenges we ran into

  • Rewriting mid-hackathon. The first version was a TypeScript/Fastify backend. Partway through, I rewrote it in Go and moved the UI to Preact, without losing the review fixes along the way: token-validation retries, case-sensitive cache keys, and the 429 cooldown after the last retry.
  • Codex CLI output. The first live check rejected perfectly good translations because the CLI also emits warning and planning items. I changed the parser to accept those as metadata while still rejecting tool calls, failed turns and unknown item types.
  • Three Twitch identities on one machine. Two broadcasters and a bot each need their own OAuth grant against a single localhost callback, plus the right scopes so the bot can post into both channels during Shared Chat.
  • Loops and ghosts. Without filtering, the bot's own posts come back as new chat and get translated again. The fix was to drop events by chatter ID and source broadcaster, and to dedupe message IDs across EventSub reconnects.
  • Sandbox limits. The sandbox the integration tests ran in couldn't bind local ports, so the tests moved to net.Pipe. They now run anywhere.

Accomplishments that we're proud of

  • It works on real Twitch. On 2026-11-03, with two real channels and a separate bot account:
    • "Hello, how are you?" and "Welcome to our bilingual stream!" went from the English channel into the Spanish one.
    • "Estoy listo para jugar." came back into the English channel as "I'm ready to play."
    • The desk read 3 sent and 0 posting errors.
  • Live on air. The translated overlay ran inside OBS on a live stream.
  • It held back the right message. A real spam bot posted a link before the relay was armed. Raidlingo showed the message on the desk, marked it "skipped — will not be replayed", and nothing reached the other chat.
  • Zero dependencies. The Go backend has no third-party modules, and all 114 tests pass with the race detector on.

What we learned

  • Most of the work is deciding what not to send. Echoes, Shared Chat mirrors, duplicates, stale queue entries and late-connecting bots all have to be handled before you can trust a bot to post on a streamer's behalf.
  • Treat a model like any other external process. Raidlingo gives it a schema, a timeout and a sandbox, and validates every reply before acting on it.
  • Plan for Twitch's rate limits and Shared Chat from the start. Retrofitting them would have meant redesigning the outbox.

What's next for Raidlingo

  • Voice. Mic capture and realtime speech-to-speech, so each streamer is dubbed into the other community's language, with subtitles and a transcribe → translate → TTS fallback. Text relay works today; voice is the next phase.
  • More languages. More than English, Spanish and Hindi, chosen per seat.
  • Hardening. Reconnect hardening and broadcast polish for long live sessions.

Built With

Share this project:

Updates

Submission history