Inspiration

ContextMod is the rule-engine mod bot that 15+ subreddit teams have been running since 2019. It's why r/mealtimevideos (60K weekly visitors) and r/piercing (600K visitors, 12K contributors) had a fighting chance against spam waves that AutoMod's regex can't catch. Then Reddit killed the free Data API in July 2023, and PRAW-era ContextMod installs started running on dying infrastructure. FoxxMD's last release was November 2022 — weeks before the paid Data API tier launched. In March 2026 Reddit announced the $1,000 Migration Bounty for PRAW → Devvit ports. On the Q1 2026 earnings call Reddit's CEO said: "we have what we call good bots on Reddit... we're porting those over to our developer platform." ContextMod is exactly what that statement names.

I got written permission from FoxxMD to port it (GitHub issue FoxxMD/context-mod#152, Discord exchange archived).

What it does

Reddit's 60K volunteer mods do 466 hours/day of unpaid moderation work (Li, Hecht, Chancellor — ICWSM 2022; sample of 21.5K mods scaled to Reddit's stated ~60K) — 73% of that work is already bot-driven and the loudest current mod ask is anti-AI-spam tooling (94-upvote top comment on May 2026 r/modnews "Mod Monthly" thread: "I want stronger tools to fight AI, not in person events"). ContextMod is exactly that tier of bot — context-gathering rules AutoMod's regex can't express.

Mods install ContextMod on their sub with one click — no Heroku, no API tokens, no shared rate limits. They write rules in JSON5 inside r/<sub>/wiki/botconfig/contextmod. ContextMod evaluates every new post and comment against those rules and takes the configured action: remove, approve, lock, comment, report, ban, userFlair, distinguish. A custom-post Observatory dashboard surfaces action telemetry live via the events:recent50 ZSET — stat cards, 24h sparkline, last 50 events with color-coded chips. Killer demo: paste a proposed rule, click "Simulate rule against history," see "would have fired N/25 (X%) on recent posts" — preview rule impact BEFORE saving wiki. Plus AI rule explainer (paste JSON5 → OpenAI returns plain-English explanation), per-event click-to-expand drill-down w/ AI summary button, mute/unmute rule from dashboard (hard-mute wired into runCheck v0.5.5), config rev diff viewer (h shortcut, LCS line diff), per-rule statistics table, mod activity attribution feed, filter chips, keyboard shortcuts, mobile-responsive, onboarding tour, light-mode toggle. v0.6.7 ships the full Phase 1+2+3+4 stack + 16 mod-UX features + every hardening pass from 13 distinct waves (S through AE). 828 tests green, 0 npm audit vulns, multi-wave adversarial review across 9 review rotations (Codex external + ultrareview cloud multi-agent + silent-failure-hunter ×3 + code-reviewer ×2 + gemini-agent ×3 + codex-rescue + vercel:performance-optimizer + type-design-analyzer + comment-analyzer ×3 + pr-test-analyzer ×2 + repo-sentinel + Explore wide-grep).

The rule engine ports the original ContextMod concept model faithfully: Run → Check → Rule → Action with postBehavior flow control (next / nextRun / stop / goto:<run>.<check>), filters (authorIs / itemIs), named rule composition, Mustache action templating with {{item.*}} / {{author.*}} / {{rules.<name>.data.*}} context. 3 MVP rule kinds (regex, author, ruleSet) + URL-dedupe repost + 8 actions (including distinguish added Wave AE for upstream parity) ship + all 3 Phase 4 stretch rules SHIPPED + live-verified 2026-05-18 (history, attribution, recentActivity on r/contextmod_vinh_dev, Vinh's commits 62a0985 + e0abd86 + 0bd59aa, +179 tests) + Phase 4.7 image-hash repost SHIPPED v0.6.0 2026-05-18 (pure-JS perceptual blockhash via upng-js + jpeg-js + blockhash-core). Upstream mhs toxicity classifier cut per Reddit PR #96 (HTTP fetch allowlist restricted to OpenAI + Gemini only — same allowlist we use for S5 AI rule explainer + V7 AI event summary).

How We built it

TypeScript + Hono + Vite served via Devvit Web (CommonJS bundle). Two-person team: Vinh on backend (rule engine, actions, handleActivity, config UX), Stephen on frontend + scaffolding + idempotency + submission. The architecture is in README.md — Mermaid flowchart TB + sequenceDiagram showing the three-stage idempotency keys + atomic config publish + dashboard webview.

  • Hono routes: /internal/triggers/* (post-submit, comment-submit, app-install, app-upgrade), /internal/cron/* (refresh-config, stats-rollup, image-hash-worker), /internal/menu/* (reload-config, recent-actions, test-rules), /internal/form/test-rules-submit, /api/recent, /api/stats, /api/health.
  • Config publish is atomic via INCR-allocated rev pointer. Mod edits wiki → refresh-config cron parses JSON5 + AJV-validates → atomic INCR allocates next rev → writes immutable cfg:rev:n → bumps cfg:current_rev pointer. Triggers pass the pre-read snapshot through to handleActivity so a publish between trigger normalization and rule execution cannot split a single event across revs (Codex H3 read-once invariant).
  • Redis storage only. Strings, hashes, sorted sets — no Lists, no Sets (Devvit constraint). The events:recent50 ring buffer is a ZSET with ZREMRANGEBYRANK trim. Per-action idempotency: cm:action:pending lease holds a random owner token so a slow worker's late releaseAction can't delete a successor's valid lease (Codex C2). commitAction retries done-write 3× w/ backoff and refuses to release pending on persistent failure (Codex C1) — prevents double-action on Devvit's at-least-once trigger delivery.
  • Observatory dashboard: React + Vite, custom-post webview. HSL design tokens, Geist + Geist Mono + Instrument Serif italic typography. Live event data via /api/recent ZRANGE. ?demo=1 synthetic-fixture path retained for screenshots/recordings. ApiResult<T> discriminated union for error UX so the dashboard preserves last-good state on backend hiccups.
  • Dry-run rule tester (Step 3.6): mod right-clicks any post/comment → menu Test rules on this item → form pre-fills thingId → submit invokes a sibling dryRunActivity() pipeline that mirrors handleActivity but forces dry-run on every action and returns a structured DryRunResult for the toast bullets. Non-contract design choice: keeps handleActivity's void signature stable while giving the form UI structured data.
  • Codex adversarial review ran end-to-end on both phases (Phase 1+2 ship and full-session retrospective). 2 CRITICAL + 7 HIGH idempotency/safety findings shipped as atomic hotfixes in-session: dry-run global authority, repost SET NX race-elimination, commitAction retries, lease owner tokens, plus 5 contract-touching fixes (publish INCR, handleActivity ConfigSnapshot, Mustache.escape default, filter regex try/catch, ParseResult wraps expandNamedRules).

Challenges

  • The first FNV-1a implementation was 32-bit and failed canonical test vectors. Codex review caught it. Rewrote with BigInt for 64-bit precision, verified against '', 'a', 'foobar'.
  • Devvit's CSP blocks eval(), which Framer Motion uses internally. Ripped framer-motion entirely, replaced with hand-rolled CSS keyframes (cmFadeUp, cmFadeLeft, cmFadeIn, cmDrawLine).
  • Sparkline blew up on Math.max(...data) when the data array was empty (stack overflow). Switched to reduce() with guards.
  • submitCustomPost deprecated splash in 0.12.23. Had to use entry + textFallback.
  • Vitest needed its own config to bypass the @devvit/start plugin (which only works in vite build mode).
  • App icon I generated via Gemini was JPEG bytes inside a .png filename — would've failed Devvit's upload validation. Caught via Codex review on Day 2, re-encoded via PIL with LANCZOS resample.
  • The first developer-portal cheat sheet I drafted invented 8 of 13 fields (tagline, category dropdown, support URL, etc.) that don't exist in Reddit's actual Developer Portal. Caught via research-agent cross-check against the official Devvit launch-guide.md. Rewrote it.
  • Reddit-API reality vs plan. Vinh shipped Phase 2 actions and caught 3 spec mismatches in live playtest: ban duration 0 = same-day unban (not permanent — permanent = omit field); lock routes via getPostById().lock() not reddit.lock(thingId); reddit.getCurrentSubredditName() doesn't exist (use (await reddit.getCurrentSubreddit()).name). All 3 fixed against the actual Reddit API surface, not the docs assumption.
  • Codex CRITICAL idempotency edges. Adversarial review caught two double-action risks: commitAction could swallow done-marker write failures and let releaseAction re-open the gate; pending lease had no owner token so a slow worker's late release could delete a successor's valid lease (third-execution race). Shipped 2 atomic hotfixes same session — retry-with-backoff + lease-owner-tokens.
  • Dry-run form submit returned thingId=undefined in live playtest. Devvit's form submission envelope is FLAT ({thingId: '...'}) not the doc-convention nested {values: {thingId}} we assumed. Caught by adding a RAW BODY log, shipped a defensive multi-shape parse, fix landed same session.

Accomplishments

  • 400+ atomic commits across 5 days (every fix is its own commit per the GitHub-activity discipline I'm using). Vinh shipped Phase 1+2+3 (3,670+ lines, 6 phase commits) in a single day; Stephen shipped Step 3.6 dry-run tester + 4 Codex CRITICAL/HIGH hotfixes + 5 contract-touching fixes the same evening. Then Wave S+T+U+V (2026-05-17) added 15 user-facing features + 16 code-review fixes across ~60 more atomic commits.
  • 828 tests green end-to-end across rule engine, actions, handleActivity orchestrator, idempotency (3-layer w/ owner tokens + retries), configStore (publish + getRecentRevs + PublishError), recentEvents, dryRunActivity, simulateRule, explainRule, explainEvent (Result), muteSet (hard-mute + isRuleMuted), modActivity, menu + form routes, all client components, OpenAI classifier regression suite, /health/deep auth gate, authorHistory degraded-flag rule-skip, dryRun marker safety, + E2E Playwright scenarios (chromium + firefox + webkit cross-browser). tsc --build clean. npm audit 0 vulnerabilities. axe-core integrated in E2E (light + dark mode scanned). Semgrep OWASP workflow on every PR + main.
  • THE killer feature: rule simulation against history. Mod pastes a proposed rule → backend fetches last 25 posts via reddit.getNewPosts → normalizes via same path live triggers use → runs the proposed rule against each → reports "Rule would fire on N/25 (X%) recent items. Examples: t3_a, t3_b, t3_c." Lets mods preview impact BEFORE saving the wiki config. Nobody else in the hackathon will have this. Demo money shot.
  • AI rule explainer + AI event summary via OpenAI. Mod pastes JSON5 → plain-English explanation via gpt-4o-mini. Each event row has an "Explain with AI" button on its drill-down. Devvit HTTP allowlist updated for api.openai.com per Reddit PR #96 (AI-provider scope).
  • Reddit cm-devvit@0.2.4 APPROVED unlisted 2026-05-18 (cm-devvit) — installable in any sub Stephen+Vinh moderate (incl. Vinh's verified r/contextmod_vinh_dev test sub). v0.6.7 source re-upload shipped 2026-05-19 (T-8, 6 days early) — Devvit cm-devvit@0.2.6 submitted for App Directory re-review so judges install the latest hardening pass (Phase 4.7 image-repost + AE Polish #1–#132); Reddit review SLA 1–7 days fits inside the 5/27 deadline.
  • Codex adversarial review ran end-to-end on both phases (Phase 1+2 ship review + full-session retrospective). 2 CRITICAL + 7 HIGH + 5 MED + 3 LOW first round; 3 HIGH + 4 MED + 3 LOW second round; ALL CRITICAL + HIGH addressed in atomic commits. Caught real safety regressions (double-action idempotency edges, dry-run safety-gate bypass, repost SET-NX race) that would have been worst in production.
  • Sookra Methodology Pillars 4 + 5 deepened with verbatim quotes from Reddit's own r/Devvit posts (1r3xcm2, 1pcm13z, 1shophd, 1sgwkm7) and Steve Huffman's Q1 2026 earnings call.
  • Privacy + ToS deployed to GitHub Pages, repo flipped public after a clean secrets audit.
  • Architecture diagram in the README is Mermaid (flowchart + sequence) — best-practice patterns from official Mermaid docs (semantic shape conventions, 4-color WCAG-AA palette, screen-reader accTitle + accDescr).
  • Domain approval came back: i.redd.it, preview.redd.it, external-preview.redd.it, external-i.redd.it are all in Reddit's global fetch allowlist — no explicit allowlist needed.
  • Live e2e scenarios captured 2026-05-16/17 on Stephen's test sub r/cm_devvit_test: Scenario G (reload-config toast w/ rule count), Scenario F (dry-run form modal + toast bullets), Scenario H (Observatory dashboard webview render).

What I learned

  • Devvit's Redis primitives are deliberately constrained. No Lists, no Sets. Designing around the absence of LPUSH forced cleaner ZSET-based ring buffers and made idempotency easier to reason about.
  • "Tech inevitable" framing only works with primary sources. Quoting Reddit's own r/Devvit posts beats quoting commentators.
  • AI-tone words are a bigger threat than I expected — u/Watchful1 publicly flagged AI-style replies as "minus points" in r/Devvit early in the hackathon. Every word in this writeup got hand-scrubbed against a blocklist.
  • Three-brain workflow (Claude as IDE driver + Codex for adversarial review + Gemini for long-context research synthesis) caught bugs I would've shipped solo: the icon JPEG mismatch, the fabricated form fields, the broken Pages links.

What's next

  • Phase 4 stretch SHIPPED + live-verified 2026-05-18: history, attribution, recentActivity rules — author-cache infrastructure backing all three (+179 tests). Vinh's commits 62a0985 + e0abd86 + 0bd59aa end-to-end verified on r/contextmod_vinh_dev.
  • Phase 4.7 image-hash repost detection SHIPPED v0.6.0 2026-05-18: perceptual blockhash in pure JS within Devvit's 30s/no-native-deps env. Vinh's Day-0 spike landed clean GO (commits 00feca5 + 19e94f0); pure-JS pipeline (upng-js + jpeg-js + blockhash-core) decodes preview.redd.it variants (320–640px) for <5MB peak RAM vs ~180MB for full-res 4K; 0–2/256 bit fidelity vs full-res. Per-sub findSimilar+recordHash lock (Polish #61) prevents RMW race on concurrent duplicate posts. Ships behind dryRun: true per RepostRule precedent — mods opt-in once they're watching the dashboard.
  • mhs toxicity rule explicitly cut per reddit/devvit-docs PR #96 (2026-05-08) — Reddit locked the HTTP fetch policy's AI-provider allowlist to OpenAI + Gemini only; api.moderatehatespeech.com falls outside the carve-out. Subs using upstream CM specifically for hate-speech filtering keep running the PRAW build.
  • Post-hackathon: open the app to all 15+ ContextMod operators FoxxMD identified; pursue Reddit Developer Funds DQE ladder ($5K-$10.5K realistic 12-mo capture); evaluate parse-time regex catastrophic-backtracking validator (Codex MED finding deferred — safe-regex npm dep adds bundling weight not worth the hackathon-window cost).

Built with

TypeScript · React · Hono · Vite · Tailwind CSS · Lucide React · Redis · Devvit Web · Reddit Developer Platform · AJV · JSON5 · Mustache · Vitest · ESLint · Prettier · GitHub Actions · GitHub Pages · Mermaid · Codex (review) · Claude Code

Built With

Share this project:

Updates