Community managers are seeing more AI-generated posts: bot-like "introductions", copy-pasted answers in help channels, and contest entries that read suspiciously polished.

In this tutorial we'll build a small Discord bot with discord.js v14 that lets moderators right-click any message and choose "Check for AI". The bot replies privately (only the moderator sees it) with an AI-likelihood score.

We deliberately don't auto-scan every message or auto-delete anything. More on why at the end.

What you'll need

  • Node.js 20.6+ (for the built-in --env-file flag)
  • A Discord application and bot token from the Developer Portal
  • An AI detection API key (we use ProofAI's, but any detector with a probability score works)
mkdir ai-check-bot && cd ai-check-bot npm init -y npm pkg set type=module # we use ES modules and top-level await npm install discord.js 
# .env (never commit this) DISCORD_TOKEN=your-bot-token DISCORD_CLIENT_ID=your-application-id PROOFAI_API_KEY=sk_live_... PROOFAI_ORIGIN=https://yourapp.com 

Step 1: Register a message context-menu command

Context-menu commands appear when you right-click a message → Apps. No slash command typing, and no need for the privileged Message Content intent, because Discord passes the target message to your bot when a moderator uses the command.

// register.js import { REST, Routes, ContextMenuCommandBuilder, ApplicationCommandType, PermissionFlagsBits } from "discord.js"; const command = new ContextMenuCommandBuilder() .setName("Check for AI") .setType(ApplicationCommandType.Message) .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages); // moderators only by default const rest = new REST().setToken(process.env.DISCORD_TOKEN); await rest.put(Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), { body: [command.toJSON()] }); console.log("Registered 'Check for AI'"); 

Run it once:

node --env-file=.env register.js 

Step 2: The detection helper

// detect.js const API_URL = "https://ai.proofai.in/api/v1/detect"; export async function detectAI(text) { const words = text.trim().split(/\s+/).filter(Boolean).length; if (words < 50) return { status: "too_short", words }; const res = await fetch(API_URL, { method: "POST", headers: { "x-api-key": process.env.PROOFAI_API_KEY, "Origin": process.env.PROOFAI_ORIGIN, "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ content: text }), }); if (res.status === 429) return { status: "rate_limited" }; if (!res.ok) return { status: "error" }; const data = await res.json(); return { status: "ok", p: data.ai_probability }; } 

Short Discord messages ("lol same", "thanks!") can't be scored meaningfully, so we refuse anything under 50 words instead of returning a misleading number.

Step 3: The bot

// bot.js import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js"; import { detectAI } from "./detect.js"; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.once(Events.ClientReady, (c) => console.log(`Logged in as ${c.user.tag}`)); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isMessageContextMenuCommand()) return; if (interaction.commandName !== "Check for AI") return; // Detection can take a few seconds, so acknowledge first (privately) await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const text = interaction.targetMessage.content ?? ""; const result = await detectAI(text); let reply; if (result.status === "too_short") { reply = `This message has only ${result.words} words. Detection needs at least 50 words to be meaningful.`; } else if (result.status === "rate_limited") { reply = "Daily detection limit reached. Try again later."; } else if (result.status !== "ok") { reply = "Couldn't check this message right now."; } else { const level = result.p >= 85 ? "High" : result.p >= 60 ? "Medium" : "Low"; reply = `**${level} likelihood of AI-generated text** (${result.p}%).\n` + `Treat this as one signal, not proof. Talk to the member before taking action.`; } await interaction.editReply(reply); }); client.login(process.env.DISCORD_TOKEN); 
node --env-file=.env bot.js 

Invite the bot with the applications.commands and bot scopes, then right-click a long message → Apps → Check for AI.

Why the reply is ephemeral

MessageFlags.Ephemeral means only the moderator who ran the command sees the result. Posting "this person used AI!" publicly, based on a probability, is a fast way to embarrass a real member and lose trust in your mod team.

Things we deliberately didn't build

  • Auto-scanning every message. It needs the privileged Message Content intent, burns your API quota on small talk, and creates a feeling of surveillance.
  • Auto-delete or auto-ban. Detectors make mistakes. False positives are more common for non-native English writers and very formal writing.
  • Public "AI" badges. Same reason.

Good uses: checking contest submissions, long "introductions" from brand-new accounts that look spammy, or answers copy-pasted into help channels.

Ideas to extend it

  • Log checks to a private #mod-log channel for transparency within the mod team
  • Add a per-moderator cooldown so nobody burns the quota
  • Support attachments (PDF/DOCX) using the extraction code from our batch-scanning post

Full API reference: ProofAI API docs · Try the free web detector.

Do you moderate a community? How are you handling AI-generated posts? Tell us in the comments. 👇

Disclosure: we're the team behind ProofAI at 4Born Solutions. This article was drafted with AI assistance and reviewed and edited by our team.