Rival Drafts
Production-grade Marvel Rivals Discord bot + infrastructure: matchmaking, tournaments, moderation, API auto-verify, two-VPS failover.
Overview
Rival Drafts is a production Discord bot I built for a Marvel Rivals PUG community. It runs 12-player matchmaking with captain drafts and per-queue ELO, full tournament brackets (single/double elim, Swiss, round robin), moderation + automod, ticket-based rank verification, Marvel Rivals API auto-confirmation, Twitch go-live notifications, daily username verification, an admin web dashboard, daily cross-VPS backups, and an active-passive failover system with automatic failback. Deployed on two VPSs via Docker Compose, operated under systemd, monitored via webhook alerts.
Highlights
- ▸ Shipped in ~10 days: 130+ commits, 60+ slash commands across 5 surfaces.
- ▸ Two-VPS infrastructure: primary + standby with health-check watchdog, auto-failover, and auto-failback.
- ▸ Daily cross-VPS database backups over rsync/SSH, 7-day retention, Discord webhook alerts on success/failure.
- ▸ Marvel Rivals API integration: auto-verify match results + daily username sync (handles renames, DMs players).
- ▸ Interactive tournament dashboard V2: single/double elim, Swiss, round robin, hidden competitive seeding, live bracket updates.
- ▸ Interactive automod panel: spam / invites / caps / bad words / mass mentions, configured via buttons + modals.
- ▸ Seven-round systematic audit pass: 40+ targeted fixes, race-condition guards, memory-leak patching.
Context
A Marvel Rivals community I was part of ran PUG nights manually — spreadsheets for ELO, mod calls in DMs, tournament brackets drawn on paper. Saturday setup was an hour before first match. I built one bot that would eat the whole workflow: queue, draft, match, report, verify, moderate, season, tournament. Then I kept going until it felt like infrastructure, not a script.
By the numbers
- ~10 days, first commit to production (Apr 11 → Apr 21, 2026).
- 130+ commits — feature work, refactors, and seven explicit audit rounds.
- 60+ slash commands: 14 matchmaking, 9 moderation, 28+ admin, 13 tournament subcommands, 1 ticket-panel.
- 10+ SQLite tables — players, per-queue stats, matches, pending reports, bans, hero pool, seasons, tournament, queue state, Twitch links, config.
- 2 VPSs, 2 Docker containers (bot + admin dashboard), 1 systemd service, 1 watchdog, 1 backup job.
- 3 third-party integrations: Marvel Rivals API, Twitch API, Discord webhooks.
Infrastructure
Designed to survive a VPS outage during a live tournament. Primary runs the bot and the dashboard. Standby receives daily DB backups and can take over automatically.
- Primary VPS: rivals-bot-bot (Node.js + discord.js + health endpoint on :3000) and rival-drafts-dashboard (web admin UI on :80), both in Docker.
- Standby VPS: backup storage at /opt/rivals-backup, watchdog script, ready-to-start copy of the bot.
- Health-check watchdog: 30s poll, 3 fails → failover, 5 successes → failback. Discord webhook alert on every transition.
- Daily rsync backup at 04:00 UTC — both SQLite DBs plus .env, docker-compose.yml, package.json. 7-day rolling archive.
- Docker image: node:20-alpine, runs as non-root 'node' user via entrypoint.sh, persistent data volume.
- Locked internal health endpoint, bind bot port to 127.0.0.1 only (reverse-proxy terrain).
Engineering highlights
The parts that moved it from "Discord bot" to "system."
- Active-passive failover with watchdog + DB sync + webhook alerts — primary down, standby takes over; primary back, failback.
- Marvel Rivals API auto-verify: after a report, poll the MR custom-room API (5 min initial delay, 3 min interval), cross-reference team rosters, auto-confirm including MVP/SVP, flag mismatches between reported and API results. 15-min manual timer skipped on API match.
- Daily username verification job: 24h cron-like sweep over all linked MR names, rate-limited (2s delay). 404 from the API → username cleared + DM to re-register.
- Tournament V2 dashboard: single interactive message with dropdown nav, rank-based hidden seeding, live bracket updates, format + BO switching without recreating the tournament.
- Interactive automod config panel: staff tune spam / invite / caps / mass-mention / bad-word policies via buttons + modals, zero config-file editing.
- Multi-queue architecture: Open (all ranks) + Pro (Rank X/A only), fully separate ELO, stats, channels, and persistence.
- Queue persistence + timer recovery: queue state saved on every join/leave, restored on bot restart with timers recalculated from elapsed time.
- Twitch integration: polls Twitch API for linked players, posts go-live notifications to a configured channel.
- Admin web dashboard: separate container, live bot stats, quick-action buttons.
Quality process
Seven explicit audit rounds after the initial feature push. Each round had a theme and a commit listing every fix.
- Round 5: ELO baseline, dodge cooldown, balance-teams safety, ticket role errors.
- Round 6: 11 fixes — format vote, ban queue, timer auth, queueId integrity, ELO falsy-check, match-ID collision, streak limits.
- Round 7: moderation client param, timer recovery, setelo scope, ready-check cleanup, tournament timer stop.
- Plus: "17 audit fixes" stability/security/reliability sweep, memory-leak hunt, polish pass for deprecation warnings + race conditions.
Architecture
discord.js v14 codebase with commands grouped by concern. Persistence consolidated on SQLite via better-sqlite3 after a mid-build migration from JSON (dead JSON backup module removed once stable). One big matchdb.js (~600 lines) owns match/player/ELO; a lean bot.db handles warnings + tickets.
- src/index.js — startup, command + event loading, recovery, periodic jobs, health endpoint, graceful shutdown.
- src/commands/ — matchmaking, moderation, admin, tickets, tournament (5 surfaces).
- src/events/ — ready, interactionCreate router, guildMemberAdd/Remove.
- src/handlers/ — buttons.js (400+ lines), tickets/ (creation, close, rank verification), tournament-v2.js (bracket logic).
- src/matchmaking.js + queue.js + elo.js + state.js — core PUG flow and rating math.
- src/rivals-api.js — Marvel Rivals API client (register verification, match verification, daily sweep).
- src/twitch-monitor.js + twitch.js — stream polling + API helpers.
- src/automod.js — filter + interactive config.
- src/utils/ — embed builders, action logger — logger writes every mod action / ELO change / admin action / ticket event to a log channel.
- Dockerfile + docker-compose.yml + entrypoint.sh — one-command deploy as non-root 'node' user.
Ops & operations
- Deploy: ssh into primary → git pull → docker compose down + build --no-cache + up -d → deploy-commands.js if slash commands changed.
- Live logs via docker compose logs -f bot.
- Standard rescue commands: /admin_cancelmatch, /admin_rewind, /admin_clearqueue, /admin_resetcooldown.
- /admin_export dumps the full dataset as CSV for offline analysis.
- /admin_seasonreset archives the current season (full players + matches snapshot into seasons table) and resets stats.
Status
Bot is finished and proven in production. Due to unrelated community disputes I am no longer involved with that server — see the blog post for the story. The bot itself is complete and ready to be redeployed for any Marvel Rivals community that would actually use it.
- Finished and production-proven.
- Currently homeless. If you run a Rivals community and want it, reach out.
- Rough pricing: free / near-free if the deployment doubles as my beta playground (see About).
What I took away
- SQLite handles "small multiplayer server" workloads effortlessly — JSON was the wrong default; the mid-build migration paid for itself immediately.
- Interactive dashboards (buttons + modals + dropdowns) beat text commands for anything staff use daily.
- Systematic audit rounds catch about 3x more bugs than the same time on feature-driven testing.
- Failover is worth the effort even for a Discord bot — downtime during a live tournament is community-trust damage you do not recover from.
- Never write the code before the legals are signed.