-
-
Mod Tool Setting: Configure AI provider (OpenAI/Gemini), toggle auto-send removal notifications, and enable/disable AI drafts from one panel
-
Suspended User ScoreCard: Site-wide suspension forces health score to 0 instantly — the scorecard surfaces the flag so mods know immediately
-
User ScoreCard: Tap any post or comment to see health score, violations, reports, alert level, and last action — no digging through mod logs
-
User ScoreCard Score based on Reports: -10 per violation and -5 per report — giving mods the full picture even when content isn't removed.
-
User Appeal AI draft mail: reads violation history, classifies intent, and drafts a context-aware reply as an internal mod note.
-
Automatic removal message for users in private:sends private modmail explaining the violated rule, based on the user's violation history.
Inspiration
Moderators on Reddit are invisible labor. Everyday, in every communities there are tons of moderators spending hours in sifting through reports, responding to modmail, also trying to remember whether a user has caused problems before! An user sends an angry appeal, and the mod has to manually search through old actions, cross-reference their history, and craft a response from scratch every single time.
I noticed with the advanced Reddit's filter, the hardest of moderation isn't catching the bad content, Reddit's built-in filters handles does stuff. the hardest part is communicating clearly with user, tracking patterns across time, and not burning out doing it.
The idea is simple: What if the mod team had an assistant that already knew the user's history before the modmail even arrived?
What it does
ReadIt-Mod is a Devvit app that turns Reddit's modmail inbox into an intelligent moderation workspace. It has three core systems that work together:
1. Automatic violation tracking
Every time a mod removes a content or a user gets reported, ReadIt-Mod silently records it in the Redis - the rule violated, the content snippet, the timestamp, and the type of removal. It also tracks ban evasion flags and Reddit's automated spam classifications from PostReport events. Over time, this builds a per-user moderation history that persists across every conversation.
2. AI-powered modmail drafts When a user sends a modmail message, the app detects the intent automatically - is it an appeal, a complaint, a question, or a scam attempt? Then it pulls the user's full violation history from the Redis, fetches the subreddit's actual rules, and sends all of that as context to the AI. The AI generates a context-aware draft reply - one that references specific prior violations, adjusts its tone based on whether the user is a first-timer or a repeat offender, and responds firmly when scam indications are detected in the message itself. The draft is posted as an internal mod notes - mods review it, edit it if needed, and send it themselves. No auto-sending to users without any human review.
3. User Scorecard A moderator-only menu item on any post or comment that shows the user's complete picture in one tap: health score (0–100%), violation count, report count, alert level, last action date, and account status flags — including whether the user is suspended site-wide, shadowbanned, or banned from the subreddit.
The health score uses the formula:
$$H = \max(0,\ 100 - 10V - 5R)$$
where $V$ is the number of tracked violations and $R$ is the number of reports. Account-level suspensions override the formula, forcing the score to 0 regardless of prior history. Higher $H$ means lower risk—a "cleaner" user profile.
Account status detection works through three independent checks: a live call to ctx.reddit.getUserByUsername() (which returns null for suspended accounts), a cross-check against the subreddit's banned user list via ctx.reddit.getBannedUsers(), and status data extracted directly from the modmail conversation's user object when available. All status results are cached in Redis for one hour to avoid redundant API calls.
The risk score—the direct measure of moderation risk—follows:
$$S_{risk} = \min(100,\ 10V + 5R)$$
Higher $S_{risk}$ means higher risk—more violations or reports escalate the user's threat level. The two scores are complementary views of the same data, related by:
$$H = \max(0,\ 100 - S_{risk})$$
Alert levels escalate as:
$$\text{alertLevel} = \begin{cases} \text{high} & \text{if } V \geq 7 \text{ or } R \geq 5 \ \text{medium} & \text{if } V \geq 5 \text{ or } R \geq 3 \ \text{low} & \text{if } V \geq 3 \text{ or } R \geq 2 \ \text{none} & \text{otherwise} \end{cases}$$
4. Automatic Removal Notifications When content is removed — by a moderator, AutoModerator, or Reddit's automated filters — the app can send a private modmail directly to the user explaining the violation. The message includes the rule violated, a professional explanation of why (using smart fallback detection when the mod doesn't provide a reason), the user's violation history count, and an escalation warning if they've accumulated multiple violations. A 1-hour per-user cooldown prevents duplicate messages. This is controlled by two settings: "Auto-Reply When Content is Removed" (master toggle) and "Auto-Send Removal Notifications" (when disabled, no modmail is sent and the removal is only tracked internally).
How we built it
ReadIt-Mod is built entirely in TypeScript on the Devvit platform, using three Devvit primitives: Triggers, Menu Items, and Redis.
The architecture has four layers:
Data layer (Redis)
All user data is stored in two Redis keys per user: violations:{username} holds the aggregated violation count, report count, computed risk score, alert level, and last action timestamp as a JSON blob. removals:{username} holds a rolling array of the last 5 removal records — each with date, rule, reason, content snippet, and removal type. A third key, user_status:{username}, caches the account status check with a 1-hour TTL so we don't hit the Reddit API on every scorecard view. A fourth key, modmail_cooldown:{username}, prevents a user from receiving multiple removal notifications in the same hour.
Tracking layer (Triggers)
ModAction fires on removelink, removecomment, approvelink, approvecomment, and banuser. Username resolution uses a 6-step fallback chain because the event.targetUser shape is inconsistent across Devvit versions — we check targetUser.name, target.author, target.authorName, author.name, and fall back to API lookups by ID if nothing else is present. PostReport captures automated Reddit filters by checking the report reason string for spam, harassment, and ban evasion patterns, and treats those as violations rather than just reports.
AI layer (HTTP)
generateAIReply builds a structured prompt with four sections: scam detection context (if triggered), the user's full violation history formatted as a readable mod report, the subreddit's actual rules fetched live from the Reddit API, and tone instructions derived from the user's profile — first-timer, repeat offender, suspended, or scam sender. The function calls OpenAI or Gemini depending on the mod's settings, with a 25-second timeout and explicit 1024-token output limit to prevent truncation.
Display layer (Menu Items)
Two menu items — one on posts, one on comments — call getUserHistory() which assembles all Redis data and runs the status checks, then formats everything into a single toast notification the mod sees instantly.
Challenges we ran into
The messageAuthorType enum problem
When the NewModMailReceived trigger fires, the event.messageAuthorType field is supposed to tell you whether the message came from a user or a moderator. In practice, the enum values are inconsistently documented across Devvit versions — we were seeing 'ParticipatingAs_MODERATOR' for our own bot replies (correct) but never seeing the expected 'ParticipatingAs_PARTICIPANT_USER' for actual user messages. We solved this by inverting the logic: instead of whitelisting known user types, we blacklist anything containing "mod" or "admin" in the string, skip messages from authors whose name matches the subreddit name, and skip internal notes. This is much more resilient to future enum changes.
fetch() to www.reddit.com is blocked in menu handlers
Our first approach for account status detection was fetching reddit.com/user/{username}/about.json. This works in trigger contexts but is silently blocked in onPress menu handlers due to Devvit's domain allowlist. The fix was switching to ctx.reddit.getUserByUsername(), which is the proper Devvit API and works everywhere. Suspended users return null or throw an exception with a message containing "suspended" — both cases are now caught and handled.
The violation score was always 100%
During testing, the User Scorecard always showed 100% health for every user. The root cause was that the ModAction trigger doesn't fire for your own actions in playtest mode — meaning Redis was never being written to. We solved this by building a debug seed menu item that writes test violation data directly to Redis, allowing the full pipeline to be tested end-to-end without waiting for real mod actions to occur.
AI replies were being cut off mid-sentence
The initial maxOutputTokens: 500 seemed sufficient on paper, but the system prompt alone consumed roughly 400 tokens, leaving only 100 tokens for the actual reply. The result was responses that ended abruptly mid-sentence. Increasing to maxOutputTokens: 1024 and restructuring the prompt to front-load the most important instructions resolved this completely.
Modmail feedback loop
When our app posted an AI draft as an internal reply, that action itself triggered NewModMailReceived again — creating a potential infinite loop. The fix was detecting that the second trigger's messageAuthorType contained "MODERATOR" (our app acting as the subreddit) and returning early. The internal note flag also helps here, but the author type check is the more reliable guard.
Redis doesn't support list operations consistently
Devvit's Redis client doesn't expose LPUSH / LRANGE in all versions. We stored the removal history as a JSON-serialized array under a single key instead, prepending new records with unshift() and capping at 5 entries manually. Less elegant, but it works reliably across all Devvit versions.
Accomplishments that we're proud of
The thing we're most proud of is the scam detection pipeline. When a user's modmail message contains indicators like "entry fee", "UPI", "₹", or shortened URLs, the AI's entire tone flips — it stops being welcoming and becomes firm, directly citing the rule violated without encouraging further engagement. This wasn't in the original plan; it emerged from noticing that our test subreddit was receiving exactly these kinds of solicitation messages in modmail.
We're also proud of the multi-layer username resolution in the ModAction trigger. The event shape from Devvit is genuinely inconsistent — sometimes the username is in targetUser.name, sometimes it's in target.author, sometimes it requires an API call to resolve from an ID. Building a robust 6-step fallback meant the tracker actually works reliably in production rather than silently failing on half of removal events.
Finally, the 1-hour Redis TTL cooldown on removal notifications is a small detail that matters a lot in practice. Without it, a user who posts three things in a row that all get removed would receive three separate modmail notifications in minutes — which reads as harassment even if each individual notification is correct.
What we learned
The Devvit platform is more capable than its documentation suggests, but also more surprising in its edge cases. The messageAuthorType enum problem, the fetch domain allowlist, the inconsistent ModAction event shapes — none of these are documented. You find them by shipping and reading logs.
We also learned that prompt engineering for moderation is genuinely hard. The AI needs to be warm to first-timers, firm with repeat offenders, and completely different (terse, rule-citing, not encouraging) with scam senders — all from the same function. Getting the tone instructions precise enough that the AI doesn't default to a generic "thank you for reaching out" opener took more iteration than all the TypeScript combined.
The biggest technical lesson was about Redis as a state machine. Devvit apps are stateless between trigger invocations — the only memory is Redis. Designing the data schema carefully upfront (what keys, what shapes, what TTLs) determines everything downstream. We had to refactor the schema twice when we realized the modmail draft function, the scorecard menu, and the AI prompt builder all needed slightly different views of the same data.
What's next for ReadIt-Mod
AutoModerator filter integration - adding AutomoderatorFilterPost and AutomoderatorFilterComment triggers so that automated removals are tracked in Redis the same way manual removals are. Currently, if AutoMod silently removes something, it's invisible to our system. This is the highest-priority next feature.
Separate settings for AutoMod vs manual removal notifications — right now both paths share the same AutoSendReplies toggle. Mods should have independent control over each.
Appeal workflow — a structured Devvit custom post type that banned users can submit, presenting the mod team with a formatted decision card showing the original offense, the user's appeal, and their full history. Replaces the current unstructured angry-modmail pattern.
Cross-subreddit violation sharing — an opt-in network where subreddits that choose to participate can query whether a user has violations on other participating subs before replying to their modmail. Built on shared Redis keys with opt-in consent from each subreddit's mod team.
Positive reinforcement queue — a weekly "top contributor" summary surfaced to mods so they can pin highlights and reward good community members. Moderation is entirely focused on removing bad things; this would be the first tool in the workflow focused on celebrating good ones. "
Built With
- devvit
- modmail
- redis
- typescript
Log in or sign up for Devpost to join the conversation.