-
-
Per-event drill-down on a LIVE dry-run repost. Shows rule chain, would-have-called action, plus the ✨ Explain with AI button (V7).
-
Observatory dashboard after Scenario B: stats table shows repost-watch/url-dedupe-30d fired 1x dry-run. Live event from r/cm_devvit_test.
-
Reload config from wiki firing live. Toast confirms rules loaded from wiki JSON5. Atomic publish via INCR-allocated rev pointer.
-
Dry-run rule tester modal. Right-click any post, see exactly which rules would fire BEFORE they fire. Zero Reddit side-effects.
-
Empty-state dashboard right after first install. Shows the starter-config snippet so first-time mods know what to paste in the wiki.
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-configcron parses JSON5 + AJV-validates → atomic INCR allocates next rev → writes immutablecfg:rev:n→ bumpscfg:current_revpointer. Triggers pass the pre-read snapshot through tohandleActivityso 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:recent50ring buffer is a ZSET withZREMRANGEBYRANKtrim. Per-action idempotency:cm:action:pendinglease holds a random owner token so a slow worker's latereleaseActioncan't delete a successor's valid lease (Codex C2).commitActionretries 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/recentZRANGE.?demo=1synthetic-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 siblingdryRunActivity()pipeline that mirrorshandleActivitybut forces dry-run on every action and returns a structuredDryRunResultfor the toast bullets. Non-contract design choice: keepshandleActivity'svoidsignature 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 toreduce()with guards. submitCustomPostdeprecatedsplashin 0.12.23. Had to useentry+textFallback.- Vitest needed its own config to bypass the
@devvit/startplugin (which only works invite buildmode). - App icon I generated via Gemini was JPEG bytes inside a
.pngfilename — 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:
banduration 0 = same-day unban (not permanent — permanent = omit field);lockroutes viagetPostById().lock()notreddit.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:
commitActioncould swallow done-marker write failures and letreleaseActionre-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 --buildclean.npm audit0 vulnerabilities.axe-coreintegrated 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_devtest 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.itare 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
LPUSHforced 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,recentActivityrules — author-cache infrastructure backing all three (+179 tests). Vinh's commits 62a0985 + e0abd86 + 0bd59aa end-to-end verified onr/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 behinddryRun: trueper RepostRule precedent — mods opt-in once they're watching the dashboard. mhstoxicity rule explicitly cut perreddit/devvit-docsPR #96 (2026-05-08) — Reddit locked the HTTP fetch policy's AI-provider allowlist to OpenAI + Gemini only;api.moderatehatespeech.comfalls 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-regexnpm 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
Log in or sign up for Devpost to join the conversation.