Discord Bots

Build an Auto-Moderation Discord Bot with Node.js

Tech Setup5 min read
TS

Tech Setup

Published August 12, 2026 · Editorial policy

Build an Auto-Moderation Discord Bot with Node.js

Managing a mid-sized developer Discord server means drowning in spam, unsolicited DM promotions, and low-effort link drops by the time you log on in the morning. Existing third-party bots either lock essential keyword filters behind paywalls or harvest user data, leaving you with no clean way to enforce custom channel rules programmatically. You are going to build a lightweight, self-hosted Node.js moderation bot using discord.js v14 that intercepts messages, scans for banned regex patterns, and automatically issues timeouts without bloating your infrastructure.

Quick TL;DR: The Core Pipeline

If you just want the working code and understand the basics of event-driven Node.js, here is the entire message inspection loop you will implement.

// index.js
const { Client, GatewayIntentBits, PermissionFlagsBits } = require('discord.js');

const client = new Client({
    intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent,
    ],
});

const BANNED_PATTERNS = [
    /discord\.gg\/[a-zA-Z0-9]+/i, // Invite links
    /t\.me\/[a-zA-Z0-9]+/i,      // Telegram links
    /free nitro/i,                 // Common scam phrase
];

const TIMEOUT_DURATION_MS = 5 * 60 * 1000; // 5 minutes

client.once('ready', () => {
    console.log(`Logged in as ${client.user.tag}`);
});

client.on('messageCreate', async (message) => {
    if (message.author.bot || !message.guild) return;

    // Skip checks if user has manage messages permission (mods/admins)
    if (message.member.permissions.has(PermissionFlagsBits.ManageMessages)) return;

    const content = message.content;
    const isViolation = BANNED_PATTERNS.some((pattern) => pattern.test(content));

    if (isViolation) {
        try {
            await message.delete();
            
            // Timeout the offender for 5 minutes
            await message.member.timeout(TIMEOUT_DURATION_MS, 'Automated moderation: Banned content detected');
            
            // Send a temporary warning in the channel
            const warning = await message.channel.send(
                `${message.author}, your message was removed and you have been timed out for 5 minutes for violating server rules.`
            );
            
            setTimeout(() => warning.delete().catch(() => {}), 5000);
        } catch (error) {
            console.error('Failed to moderate message:', error);
        }
    }
});

client.login(process.env.DISCORD_TOKEN);

To make this run, initialize your project, install the dependencies, and configure your environment.

Project Bootstrap and Dependencies

Create a clean directory for your bot and initialize a standard Node.js project. Open your terminal and run:

mkdir discord-automod
cd discord-automod
npm init -y

You need discord.js to interact with the Discord API and dotenv to manage your bot credentials safely. Install them with exact version pinning to prevent breaking changes:

npm install discord.js@14.14.1 dotenv@16.4.5

Open your package.json and add a start script so you can launch your bot easily:

"scripts": {
  "start": "node index.js"
}

Next, create a .gitignore file to ensure you never accidentally commit your bot token to version control:

node_modules/
.env

Create a .env file in the root directory. This will hold your sensitive Discord bot token:

DISCORD_TOKEN=your_bot_token_here

Configuring the Bot in the Discord Developer Portal

Before running your code, Discord needs to know that your application exists and what permissions it requires.

  1. Go to the Discord Developer Portal and click New Application. Name it something clear, like DevModBot.
  2. Navigate to the Bot tab on the left sidebar. Click Add Bot.
  3. Under the username, locate the Privileged Gateway Intents section. You must enable:
    • Presence Intent (optional, but good for tracking state)
    • Server Members Intent (required if you want to inspect member roles)
    • Message Content Intent (critical: without this, message.content will return an empty string for non-mention messages).
  4. Scroll down to Token, click Reset Token, and copy the resulting string into your .env file as DISCORD_TOKEN.
  5. Navigate to the OAuth2 > URL Generator tab.
  6. Under Scopes, check:
    • bot
    • applications.commands (if you plan to add slash commands later)
  7. Under Bot Permissions, check:
    • Manage Messages (to delete spam)
    • Timeout Members (to issue automated mutes)
    • Send Messages (to post warnings)
    • Read Message History (to parse incoming text)
  8. Copy the generated URL at the bottom, paste it into your browser, and select your development server to invite the bot.

Implementing Advanced Rule Layers

The basic regex filter catches obvious spam, but malicious actors frequently use zero-width spaces, leetspeak, or repeated identical messages to bypass simple text checks. You need a multi-layered verification check inside your messageCreate event handler.

Update your index.js to include a normalization function that strips out hidden Unicode tricks before running regex checks:

function normalizeContent(content) {
    return content
        .normalize('NFKD') // Decompose combined characters
        .replace(/[\u200B-\u200D\uFEFF]/g, '') // Remove zero-width spaces
        .toLowerCase();
}

Integrate this normalization step into your message parsing logic:

client.on('messageCreate', async (message) => {
    if (message.author.bot || !message.guild) return;
    if (message.member.permissions.has(PermissionFlagsBits.ManageMessages)) return;

    const cleanContent = normalizeContent(message.content);
    const isViolation = BANNED_PATTERNS.some((pattern) => pattern.test(cleanContent));

    if (isViolation) {
        // Enforcement block remains the same
    }
});

Handling Rate Limits and Flood Protection

Keyword matching doesn't stop someone from pasting a wall of harmless text fifty times a second. To prevent chat flooding, track message timestamps per user in memory using a Map.

Add a rate-tracking structure at the top of your file:

const userMessageCache = new Map();
const SPAM_THRESHOLD = 5; // Messages allowed
const SPAM_WINDOW_MS = 10000; // Within 10 seconds

Add this check inside your messageCreate listener, right after checking if the author is a bot:

const userId = message.author.id;
const now = Date.now();

if (!userMessageCache.has(userId)) {
    userMessageCache.set(userId, []);
}

const timestamps = userMessageCache.get(userId);
// Remove timestamps older than our window
const recentTimestamps = timestamps.filter(timestamp => now - timestamp < SPAM_WINDOW_MS);
recentTimestamps.push(now);
userMessageCache.set(userId, recentTimestamps);

if (recentTimestamps.length > SPAM_THRESHOLD) {
    try {
        await message.member.timeout(15 * 60 * 1000, 'Automated moderation: Flood/Spam detection');
        await message.channel.send(`${message.author}, you have been timed out for sending messages too quickly.`);
        // Clear their cache entry
        userMessageCache.delete(userId);
        return;
    } catch (error) {
        console.error('Failed to apply flood timeout:', error);
    }
}

Running and Testing Your Setup

Start your bot from the terminal using the script you configured:

npm start

Expected output in your terminal:

Logged in as DevModBot#1234

To test the bot safely without annoying real community members:

  1. Create a dedicated #bot-testing channel in your development server.
  2. Ensure your bot's role is dragged to the top of the Role List in Server Settings. If a regular member has a role higher than the bot, Discord's API will reject timeout and message deletion requests with a hierarchical permissions error.
  3. Send an invite link (e.g., discord.gg/example) from a secondary test account.
  4. Verify that the message instantly disappears, a warning displays briefly, and the test account receives a 5-minute timeout status badge in the member list.

If This Doesn't Work

Even with correct code, Discord's strict permission and caching models frequently cause silent failures. Check these common failure points first:

  • The bot deletes messages, but fails to timeout users: Your bot lacks the Moderate Members permission, or the bot's assigned role is positioned below the target user's highest role in the server settings hierarchy. Discord enforces strict role-based execution boundaries. Drag your bot's role above all standard member roles.
  • message.content is completely empty string: You forgot to enable the Message Content Intent toggle under the Bot tab in the Discord Developer Portal. Toggle it on, save changes, and restart your Node process.
  • The bot ignores messages entirely: Ensure your bot account has actually joined the specific channel you are testing in and that the bot's role has explicit View Channel and Read Message History permissions for that channel.

Alternatives and Prevention

Running an in-memory Node.js bot works well for single-server setups, but if your community scales past a few thousand active members, storing rate-limit states in a local Map will fail when you scale horizontally or restart your process. For production scaling, swap your local Map for an external Redis store using ioredis to manage sliding-window rate limits across multiple bot instances. Alternatively, if you want to avoid managing infrastructure entirely, look into Discord’s native Auto-Moderation feature found in Server Settings, which handles keyword filtering and spam detection at the API level without requiring custom code execution.