Game Threads — Project Story

Inspiration

I have been developing and running PRAW-based game thread bots for over a decade, starting with u/PhilsBot for r/phillies and expanding to other major Philadelphia sport subreddits — r/eagles, r/flyers, and r/sixers — plus r/athletics, r/NewYorkMets, and r/braves. Hosting the bots at my house subjects them to power and internet outages; hosting on cloud providers introduces ongoing costs. Devvit is the perfect solution to both problems, and not having to maintain my own bot management platform is a huge bonus. When Reddit announced the PRAW-to-Devvit migration program, I had the rare experience of seeing the exact tool I'd been waiting on arrive at the exact time I was ready to use it.

What it does

This application combines four sport-specific bots — MLB, NFL, NHL, and NBA — into a single Devvit app that each subreddit installs and configures for its own sport. The core module handles the daily lifecycle:

  • Off-day or game-day thread posted in the morning
  • Game thread posted a configurable number of minutes before first pitch / kickoff / puck-drop / tip-off
  • Postgame thread when the game reaches its final state

The core handles thread scheduling, sticky-pinning, mod-distinguish, optional live-discussion mode, body diffing to avoid no-op edits, and everything else that's structurally the same regardless of sport. A Sport interface abstracts the sport-specific surface: each sport module fetches its own data, narrows its own typed snapshot, and renders its own Liquid templates for titles and bodies. Mods customize behavior through a single config wiki page (per-sport YAML schema) plus a small set of universal install settings. They can override the default Liquid templates by pointing settings at their own wiki pages — they get the bot's defaults out of the box but never lose control over the final output.

Live updates run on a 5-second cron, throttled by a cheap status probe so the bot only pays the cost of a full snapshot fetch when the gate says the game state changed meaningfully. Per-sport renderers populate scoring plays, decisions, line scores, boxscores, standings, division scoreboards, no-hitter watches, highlights, and everything else readers expect from the legacy PRAW bots.

How we built it

The app is built on Devvit Web (Webbit) using the Mod Tool Template per Reddit's PRAW migration guide — Hono for routing, Vite for bundling, TypeScript throughout, Vitest for tests.

The internal architecture is enforced through 17 Architectural Decision Records (ADRs) and a catalog of mandated patterns with per-pattern code-review severity ratings. A few of the load-bearing ones:

  • Sport interface (ADR-001/003) — the only contract between core and sport modules. Sport modules export a single Sport object; a one-file registry is the sole importer. Adding a sport is a new directory and a registry entry — no core changes.
  • Wiki-config gateway (ADR-005) — all sport-specific config lives in a per-sport r/{sub}/wiki/game-threads/config-{sport} page, parsed and Zod-validated on read. Config writes are bounded to AppInstall/AppUpgrade so cron tasks can never clobber a mod's edits.
  • Unified render scope (ADR-008) — one Liquid scope per thread-type group, populated unconditionally by lifecycle + sport renderer, consumed identically by title and body compose paths. "Populate, don't curate" — every value a renderer can produce goes into the scope whether or not the default template uses it; the template, not the renderer, decides visibility.
  • Throttle gate before any expensive fetch (ADR-014) — a per-tick status probe ($\sim$2 KB) gates the per-tick snapshot fetch (up to several MB during live games). This eliminated a per-5s gumbo storm we shipped before realizing the original lifecycle ordering had the gate downstream of the snapshot.
  • Integration tests against real API fixtures (ADR-016) — every renderer change requires an integration test that runs the full composeTitle / composeBody pipeline against captured ESPN / MLB StatsAPI / NHL-official responses. Game-threads has $\sim$2,950 tests across 119 files at the time of this writing.

Development used an AI-assisted multi-agent workflow: an Architect agent (Opus, long-context) wrote specs, ADRs, and reviewed PRs; Developer agents (Codex and Sonnet) executed implementation in isolated git worktrees; a Code Reviewer agent enforced the mandated-pattern catalog. Planka tracked work cards; Obsidian held the ADR + pattern + research vault. Every PR went through Builder → Code Review → Architect acceptance, with the merge commit aggregating acceptance notes.

Challenges we ran into

  • QuickJS runtime surprises. Devvit's QuickJS sandbox produced subtle divergences from documented signatures. The most painful: settings.get<T>('selectKey') returns [T] (a single-element array) for SelectSetting types, not T as the signature claims. Caught only after a r/PhilsTest playtest. We extracted unwrap helpers and updated test mocks so the next regression is caught by CI.
  • TLS fingerprint enforcement. stats.nba.com is unreachable from Devvit's fetch because Devvit's TLS ClientHello fingerprint doesn't match a real browser; the edge rejects at the handshake before HTTP. No headers, no cookies, no proxy fixes it. We rerouted NBA to ESPN's site API entirely.
  • Markdown rendering inconsistencies. Reddit's richtext-JSON converter rejects empty markdown-table header cells with META_RTJSON_MALFORMED. Old Reddit, new Reddit, and the widget renderer each collapse whitespace differently — visible-gap formatting requires &nbsp; in table cells, supertext, and bold-in-table contexts. Two pattern entries codify the rules.
  • The "silently dropped value" defect class. Multiple times the renderer produced a value that the lifecycle's hand-curated bundle didn't carry to the template, or the template lacked the slot the renderer populated. We refactored to a unified render scope (ADR-008) and added a "slot↔value correspondence" review pattern (SV1) to structurally eliminate the class.
  • Per-player metadata layering. Sport APIs typically have two co-resident surfaces — a player-record layer with full metadata, and a participation-record layer with thin references plus per-game stats. Reading metadata from the wrong layer returns undefined silently. The codified rule (ADR-012.S9) directs every per-player metadata lookup at the player-record layer.

Accomplishments that we're proud of

  • A single Devvit deployable app supports four sports cleanly behind one interface. Adding a sport is mechanical; no core code touches.
  • $\sim$2,950 tests, 119 test files, with renderer-integration tests asserted through the full compose pipeline rather than against intermediate render-scope objects — the defects the suite catches are the ones users would actually see.
  • Mods get a default-templates-out-of-the-box experience but can override every title and body by editing wiki pages — Liquid templates, mod-owned, with the bot reseeding only on AppInstall/AppUpgrade and never touching mod-authored custom pages.
  • 17 ADRs and a maintained pattern catalog kept the architecture coherent across thousands of commits. Code review is checklist-driven; reviewers cite pattern IDs by severity.

What we learned

  • Devvit-native > PRAW-faithful. Port intent, not implementation. Cron-driven polling beats threading flow control; Hono routes beat scheduler callbacks once you embrace Webbit. Most things that felt awkward initially turned out to be the platform asking us to do them better.
  • Structurally eliminate defect classes; don't test around them. "Populate, don't curate" eliminated an entire bug family by removing the curation step.
  • Real-API fixture replay catches what synthetic data won't. A fixture constructed from assumed-correct paths will agree with the buggy code that constructed it.
  • AI-assisted development with explicit ADR discipline scales. The Architect/Developer/Reviewer separation, anchored on a pattern catalog with severity ratings, kept three apps and four sport modules coherent across multiple agent contexts.
  • The Devvit platform is opinionated and rewarding. Things you'd build yourself outside Devvit — auth, scheduling, Redis, secret storage, fetch allowlisting, settings UI — are handled by the platform. The constraints (QuickJS sandbox, fetch allowlist, no eval) are the price of getting the rest for free, and it's a good trade.

What's next for Game Threads

  • Production rollout to the seven existing PRAW target subreddits (where I currently run the game thread bots): r/phillies, r/NewYorkMets, r/braves, r/athletics, r/eagles, r/flyers, r/sixers.
  • NFL and NHL parity with MLB/NBA — their renderers are scaffolded; integration-test matrices fill in next.
  • Operator/debug menu actions (ADR-015 drafted) — force-fetch, force-recompose, dump-Redis-state, and other operations that today require waiting for the next cron tick.
  • Event comments expanded from MLB to additional sports (enhancement over praw-based bots) — the optional-Sport-interface pattern (ADR-011) makes this a per-sport opt-in without core branching.
  • Publishing the apps publicly for additional subreddits to use.
  • Open-sourcing the code for Game Threads as well as the companion apps (duplicate link removal and sidebar standings updater).

Built With

Share this project:

Updates

posted an update

This app is now LIVE and has replaced the data API bots in the following subreddits: r/sixers (NBA), r/flyers (NHL), r/eagles (NFL), and r/braves (MLB). The app is running in dry-run mode and will replace the data API bot in r/phillies pending bot account conversion to app account (u/PhilsBot name must live on!). Will be live in r/athletics and r/NewYorkMets as I have time to do the conversion over the next few days.

Log in or sign up for Devpost to join the conversation.

posted an update

NFL game threads are now working as well, though only in a private subreddit against past games. NHL and NFL have event comments (scoring plays, penalties, etc., fully configurable per subreddit), which only MLB has in the data API version of these bots.

All four sports are now working and will be rolled to live subreddits in the coming weeks.

Log in or sign up for Devpost to join the conversation.