Inspiration
Mods of any decent-sized subreddit get the same kind of ban-appeal modmail over and over: 50 words of "I didn't read the rule" wrapped in apologies. Reading them isn't hard, but it's slow, and the cadence keeps you from doing anything else with that brain. We wanted the cheapest possible upgrade for that flow — not an "AI verdict" but a 30-second-faster read.
What it does
ModWingman watches every modmail conversation that lands in a subreddit it moderates. When the message body looks like a ban appeal (regex over appeal | banned | reconsider | unban), the app drops a private mod note into the same conversation with this 4-line summary:
- TONE — sincere / dismissive / aggressive / boilerplate
- KEY POINTS — bullet points the appeal actually makes
- RED FLAGS — anything the LLM flags as suspicious (mention of alts, evasion, hostility)
- SUGGESTED ACTION —
approve | deny | escalate+ one-sentence rationale
Mods retain full discretion. The note is internal — the appealing user never sees it.
How we built it
- Devvit Web (v0.0.10) -
onModMailtrigger -/internal/triggers/modmailPOST handler, wrapped withcreateServer()so thereddit.modMail.*API has request context - Gemini 2.5 Flash via Google AI Studio (allowlisted external endpoint) - strict prompt + 4-section schema. ~590 tokens per appeal in practice
- esbuild to bundle into a single ~5MB CJS, sidestepping the @devvit/web runtime's lack of node_modules resolution
- modmail event payload is just
{ conversationId, messageId, messageAuthor, ... }- the body itself isn't included. We fetch viareddit.modMail.getConversation({ conversationId })and dereferenceconversation.messages[messageId]. (This was the bug that ate our debug session.)
Challenges we ran into
- The event has no body. The Devvit modmail trigger payload references the message via id only; the actual body comes from a second API call. We spent multiple deploy cycles chasing this.
reddit.modMail.*needs Devvit context. Plainapp.listen(port)works for HTTP but the Reddit API plugin throwsNo context found. Switching tocreateServer(app).listen()fixes it.- Recursive trigger loop. ModWingman's own reply re-fires
onModMail. The author-skip + appeal-keyword guard short-circuits that cleanly, so no exponential billing.
Accomplishments that we're proud of
End-to-end is real: a live test subreddit r/ModWingmanDemo receives a ban-appeal modmail from a throwaway account, and 5 seconds later the moderator sees the 4-line summary attached.
What we learned
The friction in shipping a Devvit app is almost entirely in matching the framework's expectations - not in the LLM call, not in the prompt. Once createServer + getConversation were wired correctly, the rest was a single fetch.
What's next
- Per-subreddit prompt overrides (so e.g. r/AskHistorians can scope what "red flags" means)
- Multi-message thread summarisation when an appeal stretches across replies
- Optional public auto-reply path (off by default) for clearly boilerplate appeals
Built With
- devvit
- esbuild
- express.js
- gemini
- google-ai-studio
- reddit-modmail
- typescript
Log in or sign up for Devpost to join the conversation.