Overview
YdotBot is a Discord bot I built for a Twitch streaming community — Discord-side moderation and community features, plus deep Twitch integration: go-live alerts, chat commands, chat auto-moderation, and a live gaming queue with an OBS browser-source overlay. It's a solo project, in continuous production use, deployed on Railway, and it's also the project I'd point to first as an example of how I actually work with AI tools: not "ask Claude to build a Discord bot," but a written spec, a phased build order, and Claude Code as a pair-programmer against that spec from Phase 0 onward.
The problem
Running a small streaming community means juggling two chat surfaces — Discord and Twitch — that don't talk to each other by default. A streamer going live should trigger a Discord announcement. Someone gifting subs on Twitch should get a shoutout. Chat moderation rules should apply consistently across both platforms rather than being configured twice, inconsistently, or not at all on one of them. And a community "who's next" gaming queue needs to be visible in an OBS overlay in real time, not buried in a Discord channel someone has to alt-tab to check.
Spec-driven development, start to finish
Before any code existed, I wrote ydotbot_plan.md — a proper spec, not a wish list: the stack and why (discord.js v14, tmi.js for Twitch chat, SQLite for persistence, Oracle Cloud's Always Free tier over a PaaS, specifically because the bot needs long-lived Discord Gateway, Twitch IRC, and EventSub connections that a sleeping/cold-starting free tier would keep dropping), a full architecture diagram down to the file layout (/commands/moderation, /services/twitch-events.ts, /services/queue.ts, and so on), a data model with every SQLite table named and its purpose stated, and a phased build order — Phase 0 (scaffolding, deploy pipeline, a "ping" command proving the whole chain works end to end) through Phase 4 (levelling, giveaways, custom commands), plus an explicit "immediate next steps" checklist to remove any ambiguity about what happens first.
That spec is what made the rest of the build tractable to do with an AI pair-programmer rather than despite one. Every commit in this repo is co-authored with Claude Code, and the spec is why that worked: Claude wasn't guessing at scope or reinventing the architecture each session, it was implementing against a document that already answered the questions that usually cause AI-assisted work to drift — what's the data model, what order do features land in, what's explicitly out of scope for this phase. Phase 0 shipped a deployed, working "ping" command on the actual VM before any real feature work started, which is the spec-driven habit that matters most in practice: prove the full path end to end before building on top of it.
The phases weren't rigid, either — the spec adapted as reality intruded. Music playback was planned into Phase 3, got built, and was later removed once the actual infrastructure cost on Railway didn't justify it (more on that below). A spec-driven process isn't "write the plan once and never deviate," it's having a shared, written reference specific enough that a deviation is a deliberate, visible decision rather than scope quietly drifting session to session.
What I built
Twitch EventSub integration
The go-live alerts and gift-sub shoutouts run over Twitch's EventSub WebSocket transport rather than polling the Twitch API on a timer. WebSocket-based EventSub is the more correct approach but comes with its own failure mode — the connection drops, Twitch rotates session IDs, and a naive implementation just silently stops receiving events until someone notices the bot's gone quiet. I built explicit reconnect handling into twitchEventSub.ts so the bot re-establishes and resubscribes automatically rather than needing a manual restart.
Twitch chat automation with tmi.js
Separate from EventSub, tmi.js drives the live chat side: one-time greetings for first-time chatters, a periodic "thanks for watching" reminder while live, and chat commands (!discord, !so <username>, !lurk). Chat automod runs here too — spam detection, link blocking, a banned-word filter — configurable independently from the Discord-side automod, since a Twitch chat's spam patterns and a Discord server's aren't the same shape.
Discord-side community and moderation
A leveling/XP system with a leaderboard, welcome messages, a support-ticket system (/ticket create, /ticket close), and moderation commands (/kick, /ban, /timeout, /warn, /purge) alongside Discord's own auto-moderation for spam, caps, links, and banned words, with configurable escalation — first offence deletes the message, second times the user out, third bans. A full mod-log gives a server owner an audit trail rather than having to trust that moderation happened.
Gaming queue with a live overlay
A !join/!leave community queue in Twitch chat, with an OBS browser-source overlay served over HTTP (overlayServer.ts) so the queue is visible on stream in real time, protected by a secret key in the URL. Mod controls (!next, !lock, !remove, !clear, !unlock) let a moderator manage the queue without touching the streamer's own setup mid-broadcast.
Cost-driven refactors
Music playback (musicService.ts, playlistService.ts) was in the original scope but got removed later — voice-channel audio streaming has a real infrastructure cost on a hosting platform like Railway, and for a small community it wasn't earning its keep against that cost. Cutting a working feature because the cost-to-value ratio didn't hold up is a call I don't regret; it's easy to keep shipping features and harder to admit one isn't worth what it costs to run.
Engineering decisions
TypeScript compiled to JS for command loading
Commands are written in TypeScript and compiled to dist/ for the running bot, rather than run directly via a TS loader in production. It's a small decision but it keeps the production process simple and predictable — node dist/index.js under pm2, no runtime TS compilation step to go wrong on a restart.
Two separate automod configs, not one shared one
Discord automod and Twitch automod are configured and run independently rather than sharing one rule set. Chat culture and spam patterns differ enough between the two platforms — caps-lock is a much bigger problem in a fast-moving Twitch chat than in Discord — that forcing one config to fit both would have meant compromising on both.
Stack
| Layer | Choice |
|---|---|
| Language | TypeScript · Node.js |
| Discord | discord.js v14 · @discordjs/voice |
| Twitch | Twitch EventSub (WebSocket) · tmi.js |
| Overlay | Express (HTTP server for OBS overlay) |
| Deployment | Railway · pm2 |
| Process | Written spec (ydotbot_plan.md) · phased delivery · Claude Code pair-programming |
Reflections
The EventSub reconnect logic is the part of this project I'd point to first as an engineering outcome — it's the difference between "works in a demo" and "works unattended for months," which is the actual bar for something running in production for a live community. But the process behind the whole build is what I'd point to first when talking about how I use AI tools day to day: writing the spec before the code meant Claude Code was implementing against a clear architecture and a clear phase boundary, not filling in gaps with its own assumptions. The music-removal decision is a good example of why that matters — it was a scoped, visible deviation from the original plan, made deliberately once the cost was known, not a quiet drift that a less structured process would have let slide unnoticed.