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 ACTIONapprove | 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) - onModMail trigger - /internal/triggers/modmail POST handler, wrapped with createServer() so the reddit.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 via reddit.modMail.getConversation({ conversationId }) and dereference conversation.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. Plain app.listen(port) works for HTTP but the Reddit API plugin throws No context found. Switching to createServer(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

Share this project:

Updates