A headless Twitch bot that automatically participates in Marbles on Stream
(MOS) lobbies across many live channels, with a modern web dashboard for
monitoring and control. Designed to run as a single Docker container on an
Unraid server, alongside — not instead of —
rdavydov/Twitch-Channel-Points-Miner-v2.
Legal notice. This tool automates
!playin MOS lobbies. Use it responsibly. You are responsible for the Twitch accounts you operate. Some streamers may consider bot play unwelcome — honour their rules via the configurable blacklist. MOSBot respects Twitch's Developer Services Agreement and enforces per-channel cooldowns to avoid spam.
- Every
discovery.intervalMinutes, queries the Twitch Helix API for live streams in the Marbles on Stream category (topmaxStreams, minimumminViewers). - Dynamically joins / parts Twitch IRC channels so the joined set equals the discovery result (diff algorithm, no full reconnect).
- Observes chat passively. When ≥
lobby.minPlayersdistinct users type!playin a rollinglobby.windowSecondswindow, the bot sends exactly one!play. Then a per-channel cooldown oflobby.cooldownSecondsapplies. - Never spams, never sends first, never queues when rate-limited.
See config.example.yaml for every knob.
New here? Follow the detailed step-by-step guide in docs/INSTALL.md — from Twitch app registration to a running container in ~15 minutes.
- An Unraid server (any recent 6.x) — or any Docker host.
- A Twitch account (verified email, 2FA enabled if you run a verified bot).
- A new Twitch Developer App registered at https://dev.twitch.tv/console/apps with Device Code Flow enabled. Note the Client ID only (no secret required for DCF).
MOSBot registers a separate OAuth grant from the Channel-Points-Miner on the same Twitch account, so they do not invalidate each other. See Running alongside the Channel-Points-Miner.
- Register a Twitch Developer App and copy the Client ID:
- Open https://dev.twitch.tv/console/apps and sign in with any Twitch account (the Client ID is not tied to the bot account).
- Click Register Your Application and fill in:
- Name: free choice (e.g.
MOSBot Personal), must be unique across all Twitch apps. - OAuth Redirect URLs:
http://localhost— Device Code Flow does not use this URL, but the field is required. - Category:
Chat Bot - Client Type:
Confidential
- Name: free choice (e.g.
- Click Create → back in the list, click Manage next to your app → copy the Client ID (30-character hex string). You do not need a Client Secret; MOSBot uses Device Code Flow.
- Save this Client ID for the
TWITCH_CLIENT_IDfield below.
- In Unraid: Apps → Add Container → Template URL and paste:
https://raw.githubusercontent.com/patriqcs/unraid-templates/main/mosbot.xml - Generate secrets (run on any Docker host):
# 32-byte base64 encryption key: docker run --rm node:20-alpine node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" # Dashboard password hash (argon2id): docker run --rm -it node:20-alpine sh -c "npm i -g argon2-cli >/dev/null && argon2-cli hash -p 'your-password'"
- Paste
TWITCH_CLIENT_ID(from step 1),ENCRYPTION_KEY, andDASHBOARD_PASSWORD_HASHinto the template and click Apply. - Drop a
config.yamlinto/mnt/user/appdata/mosbot/config/(copyconfig.example.yamlas a starting point). - Open
http://<unraid-ip>:8787, log in, go to Accounts → primary → Login, enter the 6-digit code onhttps://twitch.tv/activate, and approve. The bot reports online within 10 seconds; discovery starts on the next interval.
For a fully detailed walkthrough with troubleshooting, see docs/INSTALL.md.
Placeholder for screenshots:
- Install the Docker Compose Manager plugin on Unraid (or use any Compose-capable host).
- Clone this repo locally, copy
config.example.yamlto/mnt/user/appdata/mosbot/config/config.yaml, and adjust. - Copy
docker/docker-compose.yml+ create a sibling.envfrom.env.example. docker compose up -d.
All keys, defaults, and validation rules are defined in
packages/shared/src/config.ts (zod
schema). The YAML file supports ${ENV_VAR} interpolation.
| Section | Key | Default | Notes |
|---|---|---|---|
discovery |
intervalMinutes |
3 | Helix poll cadence |
discovery |
maxStreams |
10 | Upper bound on tracked channels |
discovery |
minViewers |
30 | Skip streams below this count |
discovery |
maxViewers |
null |
Skip streams above this count; null = no cap |
discovery |
language |
null |
ISO code(s), comma-separated (e.g. de,en), or null for any |
discovery |
sortBy |
most-viewers |
most-viewers or least-viewers |
lobby |
windowSeconds |
30 | Rolling-window size |
lobby |
minPlayers |
4 | Distinct users to trigger |
lobby |
cooldownSeconds |
180 | Per-channel cooldown |
ratelimit |
userChatBudgetPer30s |
16 | Margin below 20/30s cap |
ratelimit |
verifiedBot |
false |
Unlocks 45/30s cap |
coexistence.pointsMiner.enabled |
true |
Health-card visibility | |
channels.whitelist |
[] |
If non-empty, ONLY these | |
channels.blacklist |
[] |
Case-insensitive logins | |
accounts[].enabled |
true |
Per-account toggle | |
server.port |
8787 | Dashboard + API | |
logging.level |
info |
Runtime-editable from dashboard | |
logging.rotateDays |
14 | pino-roll retention | |
database.path |
/data/mosbot.db |
SQLite file |
- Overview — big tiles (plays today, lobbies, channels, uptime), live event feed streamed over WebSocket, start/stop toggle.
- Streams — sortable table of discovered streams (viewers, language,
joined-state, per-channel
!playcounter). - Stats — line chart of plays/hour, bar chart of top channels, totals for 24h / 7d / 30d.
- Logs — live tail with level filter + substring search. The log level can be changed at runtime (no container restart).
- Accounts — per-account login via Device Code Flow (code + verification
URI displayed; visit
twitch.tv/activate). - Settings — pointer to the config file and hot-reload behaviour.
The target flow is entirely GitHub-driven — you do not build images locally.
- Fork or create the repository from this scaffold.
- Enable Actions (on by default for new repos). Ensure Settings → Actions → General → Workflow permissions is set to Read and write permissions so the workflow can publish to GHCR.
- Register a Twitch Dev App with Device Code Flow enabled and copy the Client ID.
- Tag a release:
CI builds and publishes:
git tag v0.1.0 git push --tags
ghcr.io/<owner>/mosbot:0.1.0(immutable)ghcr.io/<owner>/mosbot:latest(rolling) for bothlinux/amd64andlinux/arm64.
- Make the GHCR package public (GitHub → your profile → Packages → mosbot → Package settings → Change visibility → Public) — or keep it private and configure Unraid container credentials.
- Install on Unraid via Path A or Path B above.
- Update: new git tag → new image in GHCR. On Unraid, Community
Applications shows an "update ready" icon; one click pulls the image
and recreates the container with the same volumes. For the Compose
path:
docker compose pull && docker compose up -d.
MOSBot is designed to coexist with rdavydov/Twitch-Channel-Points-Miner-v2
on the same Unraid host and the same Twitch user account. Why this
is safe:
| Aspect | Points-Miner | MOSBot |
|---|---|---|
| Auth | Private web login (user/pass → cookie, Twitch's own web client_id) | Device Code Flow with a separate Twitch Dev App |
| Primary API | GraphQL + PubSub/EventSub | Helix + IRC |
| Chat behaviour | Reads (watch-streak); practically never sends | Sends !play rarely, throttled |
| Appdata | /mnt/user/appdata/twitch-miner/ |
/mnt/user/appdata/mosbot/ |
| Port | e.g. 5000 | 8787 |
Twitch stores one refresh token per (user, client_id) pair — because the
two bots use different Dev Apps, their OAuth grants do not overwrite each
other. Twitch also allows multiple concurrent IRC connections per user, so
both bots can JOIN the same channel independently.
- Miner container appdata path (
/mnt/user/appdata/twitch-miner/) is never mounted into MOSBot — the MOSBot container only writes to/mnt/user/appdata/mosbot/. - The Miner's cookie jar and MOSBot's encrypted SQLite token store live on different paths.
- MOSBot's default chat rate-limit budget (
userChatBudgetPer30s: 16) leaves headroom below Twitch's 20/30 cap, so any rare Miner-initiated chat message cannot push the account over the limit. - The dashboard's Coexistence card surfaces whether the Miner appdata path is readable (sanity check) and whether any chat sends from the Miner have been observed (warning above 0/hour).
| Symptom | Likely cause | Fix |
|---|---|---|
| Both bots disconnect IRC in a loop | Rare; one account banned from a channel — not a cross-bot issue | Remove the channel from MOSBot's blacklist / check Miner target list |
| 401 on Helix | ENCRYPTION_KEY rotated without re-login |
Accounts → Login again (DCF) |
| Channel muted us | Streamer rules — bot behaviour unrelated | Add channel to channels.blacklist |
/api/health— JSON health check (ok,db,accounts,uptime,counts)./metrics— Prometheus scrape endpoint:mosbot_plays_sent_total{account, channel}mosbot_lobbies_detected_total{channel}mosbot_rate_limited_total{account}mosbot_channels_joined{account}(gauge)mosbot_discovery_duration_seconds(histogram)
- Structured logs via pino; redacts tokens/cookies. Daily rotation to
/logs/mosbot.log.
pnpm install
pnpm setup:hooks # one-time: install lefthook pre-commit hooks
pnpm dev # runs bot + web concurrently
pnpm test # vitest (watch: pnpm --filter @mosbot/bot test:watch)
pnpm typecheck
pnpm lintThe bot listens on 8787; the Vite dev server on 5173 proxies /api
and /metrics to the bot.
- Append a new entry under
accounts:inconfig.yaml:- name: alt enabled: true clientId: ${TWITCH_CLIENT_ID}
- Restart the container.
- Open Accounts → alt → Login and complete DCF.
See SECURITY.md.
SQLite is the single source of truth. Copy /data/mosbot.db and query with
any SQLite client. Views daily_plays and top_channels are pre-defined.
- Fork this repo on GitHub.
- Ensure Actions is enabled (default) and Workflow permissions → Read and write.
- Tag a release:
git tag v0.0.1 && git push --tags. - Wait for CI to publish
ghcr.io/<you>/mosbot:0.0.1. - Update the
<Repository>and<TemplateURL>inunraid/mosbot.xmlto your fork's owner. - Install via Template URL on Unraid.
Can I run multiple instances of MOSBot?
Yes, but each needs its own appdata volume, unique container name, unique
ENCRYPTION_KEY, and a unique port mapping.
Does MOSBot have a winning strategy? No. MOS outcomes are RNG — MOSBot just ensures you are entered in every lobby you care about.
Can the dashboard be exposed to the internet? Use a reverse proxy with additional authentication. The built-in argon2id login is not designed for direct public exposure.
MIT — see LICENSE.

