by Ghostwire Systems
A Discord bot for pickem groups that watches the NFL schedule and posts to a channel when something changes — flex moves, kickoff time changes, postponements/cancellations, added or removed games — and announces the moment the new season's schedule drops.
- Polls a provider ladder: ESPN site.api scoreboard first, nflverse
games.csvif ESPN is down, balldontlie NFL v1 if a key is configured. One poll's data always comes from one provider — never a merge. - Normalizes every provider into the same game record, keyed by
seasonType:week:AWAY@HOMEso a provider failover doesn't read as the whole schedule changing. - Diffs against the last snapshot (
data/snapshot-<year>.json) and posts embeds to the configured channel, tagging@everyone. - A wave of 25+ new games on an empty snapshot is announced as one "schedule is out!" post, not 285 individual ones.
- Kickoff times post as Discord
<t:…>timestamps, so everyone sees their own local time. - First run captures a baseline silently — no alert spam for games everyone already knows about.
- Alerts fire on date, kickoff time, and opponent only. Stadium and TV-network strings churn on their own ("GEHA Field at Arrowhead Stadium" -> "Arrowhead Stadium") and never raise an alert — they're still stored and shown by
/schedule. - Snapshots only save after alerts post, so a Discord hiccup re-alerts next tick instead of swallowing the change.
Hard-won rules from production incidents are baked in: ESPN requests always pin the season with an explicit dates= param, and any payload that declares a different season/week than requested is refused, not trusted.
- Create the bot: Discord Developer Portal → New Application → Bot → copy the token. No privileged intents needed.
- Invite it: OAuth2 → URL Generator → scope
bot→ permissions View Channels, Send Messages, Embed Links, Mention Everyone → open the URL, pick your server. - Configure:
cp .env.example .env # fill in DISCORD_TOKEN + DISCORD_CHANNEL_ID npm install - Run:
npm start # poll forever (every POLL_MINUTES, default 30) npm run once # single poll, then exit (good for cron) npm run dry-run # single poll, print messages to console, no Discord
This repo ships with .github/workflows/poll.yml, which runs npm run once every 30 minutes on GitHub's runners and commits the updated snapshot back to the repo. To use it on your own fork: add DISCORD_TOKEN and DISCORD_CHANNEL_ID under Settings → Secrets and variables → Actions. Secrets are encrypted and never visible in the public repo or its logs.
Interactive commands run serverless on a Cloudflare Worker (worker/) via Discord's HTTP interactions endpoint — no always-on bot process. Schedule commands read the snapshot/changelog committed to this repo; score commands hit the providers live.
| Command | Channel | What it does |
|---|---|---|
/schedule [week] [phase] |
schedule | The week's slate, local kickoff times + network |
/kickoff team [week] |
schedule | When a team's (next) game starts |
/deadline [week] |
schedule | First kickoff of the week — the pick lock |
/changes |
schedule | Recent schedule changes the bot alerted on |
/released |
schedule | Has the season schedule dropped yet? |
/status |
schedule | Last poll, provider, games tracked |
/score team [week] |
scores | Live or final score, and who won |
/apis |
scores | Which providers are responding right now |
Setup: invite the bot with the applications.commands scope, npm run register (uses DISCORD_TOKEN, optional DISCORD_GUILD_ID for instant registration), then from worker/: npx wrangler deploy, set the secrets listed in wrangler.toml (npx wrangler secret put …), and paste the worker URL into the app's Interactions Endpoint URL in the Discord Developer Portal. Channel scoping is optional — leave the *_CHANNEL_IDS secrets unset to allow commands anywhere.
- SQL/CSV generator — an admin registers an example
INSERTfrom their own schedules table; on changes the bot attaches the matchingUPDATEstatement, and on schedule release it attaches a full.sql/.csvof inserts for their schema. (Queued 2026-07-14 — MVP is alert + tag only.) - Subscribe/unsubscribe per channel
- Multi-guild support with per-guild channels
- Automatic score / result posting (the
/scorecommand exists; push posts don't yet)
Private — Ghostwire Systems.
