Guide

How to Add Content Moderation to a Discord Bot

·9 min read

Discord servers with open communities face a constant moderation challenge. Manual moderation doesn't scale, and basic keyword filters miss context, slang, and images entirely. This guide shows how to add automated content moderation to a Discord bot using discord.js and the Vettly API.

What You'll Build

By the end of this guide, your bot will:

  • Check every message in designated channels against your moderation policy
  • Auto-delete messages that are blocked by the policy
  • Flag borderline messages for moderator review
  • Log all moderation decisions with audit trails
  • Handle image moderation for attachments

Prerequisites

You'll need a Discord bot token (from the Discord Developer Portal) and a Vettly API key.

installShell
npm install discord.js @vettly/sdk

Basic Message Moderation

The core loop listens for new messages and checks them against your policy:

bot.tsNode.js
import { Client, GatewayIntentBits } from 'discord.js';
import { Vettly } from '@vettly/sdk';
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
const vettly = new Vettly(process.env.VETTLY_API_KEY);
client.on('messageCreate', async (message) => {
// Skip bot messages
if (message.author.bot) return;
const result = await vettly.check({
content: message.content,
policy: 'discord-community',
});
if (result.action === 'block') {
await message.delete();
await message.channel.send({
content: `Message removed: violates community guidelines.`,
});
}
if (result.action === 'flag') {
// Forward to a mod-log channel for human review
const modChannel = message.guild?.channels.cache.find(
(c) => c.name === 'mod-log'
);
if (modChannel?.isTextBased()) {
await modChannel.send(
`Flagged message from ${message.author.tag} in #${message.channel.name}:\n> ${message.content}\nDecision ID: ${result.decisionId}`
);
}
}
});
client.login(process.env.DISCORD_TOKEN);

Image Moderation

Discord messages can include image attachments. Check them in the same message handler:

image-check.tsNode.js
// Inside the messageCreate handler, after text check
const imageAttachments = message.attachments.filter((a) =>
a.contentType?.startsWith('image/')
);
for (const attachment of imageAttachments.values()) {
const imgResult = await vettly.check({
imageUrl: attachment.url,
policy: 'discord-community',
});
if (imgResult.action === 'block') {
await message.delete();
await message.channel.send({
content: 'Image removed: violates community guidelines.',
});
break;
}
}

Slash Command for Reporting

Add a /report slash command so users can flag messages to moderators:

commands/report.tsNode.js
import { SlashCommandBuilder } from 'discord.js';
export const reportCommand = new SlashCommandBuilder()
.setName('report')
.setDescription('Report a message to moderators')
.addStringOption((opt) =>
opt.setName('message_id').setDescription('Message ID to report').setRequired(true)
)
.addStringOption((opt) =>
opt.setName('reason').setDescription('Why are you reporting this?').setRequired(true)
);
export async function handleReport(interaction) {
const messageId = interaction.options.getString('message_id');
const reason = interaction.options.getString('reason');
await vettly.reports.create({
contentId: messageId,
reason,
reportedBy: interaction.user.id,
});
await interaction.reply({
content: 'Report submitted. Moderators will review it.',
ephemeral: true,
});
}

Rate Limiting and Performance

Discord bots in large servers can process hundreds of messages per second. To avoid overwhelming your moderation API:

  • Batch where possible: if a message has both text and an image, send them in one check call
  • Skip trusted channels: configure channels that don't need moderation (e.g., admin-only channels)
  • Cache repeat offenders: if a user is repeatedly flagged, escalate to auto-block without an API call
config.tsNode.js
// Channels to skip moderation
const SKIP_CHANNELS = new Set([
'mod-log',
'admin-chat',
'bot-commands',
]);
// In messageCreate handler:
if (SKIP_CHANNELS.has(message.channel.name)) return;

Audit Logging

Every moderation decision returns a decisionId. Log these to a database or a dedicated Discord channel so you have a full audit trail for disputes and appeals:

audit.tsNode.js
async function logDecision(message, result) {
await db.moderationLogs.create({
guildId: message.guild.id,
channelId: message.channel.id,
userId: message.author.id,
content: message.content,
action: result.action,
categories: result.categories,
decisionId: result.decisionId,
timestamp: new Date(),
});
}

Next Steps

  • Custom policies: create different policies for different channel types (e.g., stricter for general chat, relaxed for memes)
  • Appeal workflow: let users contest moderation decisions through a ticket system
  • Dashboard integration: use the Vettly dashboard to monitor moderation metrics across all your servers

Moderate your Discord server with Vettly

Text, image, and video moderation in one API. Free tier includes 15,000 decisions per month — enough for most community servers.