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-fileflag) - 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-logchannel 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.
Top comments (0)