Get your free server today! View Plans →
Home Plans Blog About Contact Panel Join Discord
Discord Bots

How to build a Discord moderation bot

A hands-on guide to building a Discord moderation bot with kick, ban, timeout and warn commands, shown in both discord.py and discord.js.

How to build a Discord moderation bot

By the end of this guide you'll have a working Discord moderation bot that can kick, ban, time members out and warn them, with every action written to a mod-log channel. We'll build it twice, once in Python with discord.py and once in JavaScript with discord.js, so you can pick the language you're comfortable with. This is aimed at people who can already run a script and edit a config file but have not built a bot that takes real moderation actions yet.

What your bot needs before it can moderate

A moderation bot is different from a fun bot. It actually changes the server state, so Discord is strict about what you're allowed to do. Two things decide whether an action works: the bot's intents and its permissions.

Intents tell Discord which events your bot wants to receive. For moderation you mostly need the basics plus the members intent, which lets the bot see who is in the server and resolve their roles. Open the Discord Developer Portal, click your application, go to the Bot tab, and turn on Server Members Intent. You don't need the message content intent if you're using slash commands, and slash commands are what we'll use here.

Permissions are what the bot can do once it's in a server. When you generate the invite link, give it these:

One rule trips up everyone, so learn it now. A bot can only act on a member whose highest role sits below the bot's own highest role. If your bot's role is near the bottom of the list, it can't ban anyone above it, including admins. Drag the bot's role up in Server Settings, Roles so it sits above the people it needs to manage.

Step 1: Set up the project

Create a folder and keep your token in an environment variable. Never paste the token straight into your code, and never commit it to git.

For Python, install the library:

python -m pip install -U discord.py

For Node, set up a project and install discord.js:

npm init -y
npm install discord.js

Put your token in a file the code reads at startup. In Python you can export it in your shell, and in Node a .env file with the dotenv package is common. Either way the token stays out of the source.

Step 2: A bot that registers slash commands

Slash commands are the modern way to take input. They give you a nice UI, autocomplete, and Discord checks permissions for you. Here's the discord.py skeleton with a kick, ban and timeout command.

import os
import discord
from discord import app_commands
from discord.ext import commands

intents = discord.Intents.default()
intents.members = True

bot = commands.Bot(command_prefix="!", intents=intents)

LOG_CHANNEL_ID = 123456789012345678  # your mod-log channel id

@bot.event
async def setup_hook():
    await bot.tree.sync()  # registers slash commands with Discord

@bot.tree.command(description="Kick a member")
@app_commands.describe(member="Who to kick", reason="Why")
async def kick(interaction: discord.Interaction, member: discord.Member, reason: str = "No reason given"):
    await interaction.response.defer(ephemeral=True)
    await member.kick(reason=reason)
    await interaction.followup.send(f"Kicked {member} for: {reason}")

bot.run(os.environ["DISCORD_TOKEN"])

The discord.js version does the same job. You register the commands with Discord, then handle them when an interaction arrives.

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

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

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'kick') {
    const member = interaction.options.getMember('member');
    const reason = interaction.options.getString('reason')?? 'No reason given';
    await interaction.deferReply({ ephemeral: true });
    await member.kick(reason);
    await interaction.editReply(`Kicked ${member.user.tag} for: ${reason}`);
  }
});

client.login(process.env.DISCORD_TOKEN);

Registering the actual command options (the member and reason fields) in discord.js is usually done in a separate deploy script with SlashCommandBuilder and the REST API. Run that once whenever you add or change a command.

Step 3: Check the caller's permissions first

Never assume the person running a command is allowed to. Even if you set command permissions in the server, check inside the code so a misconfiguration can't hand a normal member a ban button. In discord.py you can use a decorator that fails cleanly if the caller lacks the permission.

@bot.tree.command(description="Ban a member")
@app_commands.checks.has_permissions(ban_members=True)
@app_commands.describe(member="Who to ban", reason="Why")
async def ban(interaction: discord.Interaction, member: discord.Member, reason: str = "No reason given"):
    await interaction.response.defer(ephemeral=True)
    await member.ban(reason=reason)
    await interaction.followup.send(f"Banned {member} for: {reason}")

In discord.js you check the member's permissions by hand at the top of the handler:

if (!interaction.memberPermissions.has(PermissionFlagsBits.BanMembers)) {
  return interaction.reply({ content: 'You cannot ban members.', ephemeral: true });
}

You should also stop someone moderating a person above themselves in the role list, and stop the bot trying to action someone above the bot. A quick guard catches both:

if (member.roles.highest.position >= interaction.member.roles.highest.position) {
  return interaction.reply({ content: 'That member ranks at or above you.', ephemeral: true });
}

Step 4: Mute someone with a timeout

Discord has a built in timeout, sometimes called "communication disabled until". A muted member can't talk or join voice until the clock runs out, and you don't need a special muted role. The maximum is 28 days. In discord.py you pass a duration:

import datetime

@bot.tree.command(description="Time a member out")
@app_commands.checks.has_permissions(moderate_members=True)
@app_commands.describe(member="Who", minutes="How many minutes", reason="Why")
async def timeout(interaction: discord.Interaction, member: discord.Member, minutes: int, reason: str = "No reason given"):
    await interaction.response.defer(ephemeral=True)
    until = datetime.timedelta(minutes=minutes)
    await member.timeout(until, reason=reason)
    await interaction.followup.send(f"Timed out {member} for {minutes} min: {reason}")

The discord.js equivalent takes the duration in milliseconds:

const minutes = interaction.options.getInteger('minutes');
await member.timeout(minutes * 60 * 1000, reason);
await interaction.editReply(`Timed out ${member.user.tag} for ${minutes} min.`);

To remove a timeout early, call member.timeout(None) in Python or member.timeout(null) in Node.

Step 5: A simple warn system

Warnings are softer than a kick. You want them to stick around so a repeat offender's history is visible. For a small bot, a JSON file is fine. Store warnings keyed by user id. Here's the Python version.

import json

def load_warns():
    try:
        with open("warns.json") as f:
            return json.load(f)
    except FileNotFoundError:
        return {}

def save_warns(data):
    with open("warns.json", "w") as f:
        json.dump(data, f)

@bot.tree.command(description="Warn a member")
async def warn(interaction: discord.Interaction, member: discord.Member, reason: str):
    data = load_warns()
    user_warns = data.setdefault(str(member.id), [])
    user_warns.append(reason)
    save_warns(data)
    await interaction.response.send_message(
        f"Warned {member}. They now have {len(user_warns)} warning(s).", ephemeral=True
    )

The Node version follows the same shape with fs.readFileSync and fs.writeFileSync, storing an array per user id. A JSON file works until the bot gets busy. Once you have a few thousand entries or you run the bot on more than one process, move to SQLite, which is a single small file and handles concurrent reads without you doing much extra work.

Step 6: Log every action to a mod-log channel

A moderation bot that leaves no trail is a problem waiting to happen. Send a message to a dedicated channel every time the bot kicks, bans, times out or warns someone. Record who did it, who it was done to, the reason, and the time. A helper keeps this tidy in discord.py:

async def log_action(action, moderator, target, reason):
    channel = bot.get_channel(LOG_CHANNEL_ID)
    if channel is None:
        return
    embed = discord.Embed(title=f"Member {action}", color=discord.Color.red())
    embed.add_field(name="User", value=str(target))
    embed.add_field(name="Moderator", value=str(moderator))
    embed.add_field(name="Reason", value=reason, inline=False)
    embed.timestamp = discord.utils.utcnow()
    await channel.send(embed=embed)

Call log_action("banned", interaction.user, member, reason) right after the ban goes through. In discord.js you build a similar embed with EmbedBuilder and send it to the channel you fetch by id. Keep the log channel private so only your staff can read it.

Step 7: Handle errors gracefully

Things will go wrong, and a crashed command leaves the user staring at "This interaction failed". Catch errors and reply with something useful. In discord.py you can attach a global handler to the command tree:

@bot.tree.error
async def on_app_error(interaction: discord.Interaction, error):
    if isinstance(error, app_commands.MissingPermissions):
        msg = "You don't have permission to do that."
    elif isinstance(error, discord.Forbidden):
        msg = "I can't do that. Check my role is above the target and I have the permission."
    else:
        msg = f"Something went wrong: {error}"
    if interaction.response.is_done():
        await interaction.followup.send(msg, ephemeral=True)
    else:
        await interaction.response.send_message(msg, ephemeral=True)

In discord.js, wrap the action in a try and catch block and read the error code. A 50013 means "Missing Permissions" and almost always points back to the role hierarchy, not your code.

Troubleshooting

These are the errors we see most often when people first ship a moderation bot.

SymptomCauseFix
Missing Permissions (error 50013)The bot's role is too low, or it lacks the permissionDrag the bot's role above the target's role and confirm Kick, Ban and Moderate Members are granted
Cannot ban an admin or ownerThat person's top role is at or above the bot'sThe bot can never action someone ranked higher than itself. The owner can't be banned at all
Slash commands don't appearCommands were never synced, or you're waiting on the global cacheSync to one guild while testing for instant updates. Global sync can take up to an hour
This interaction failedYou took longer than 3 seconds to respondCall defer first, then send your real answer with a followup
member comes back as None or nullThe Server Members intent is offEnable it in the Developer Portal and in your intents in code
Privileged intent error on startupYou requested an intent you didn't enable in the portalTurn the intent on in the Bot tab, then restart

If a command works in your test server but not a friend's, the usual culprit is role position. Have them check where the bot's role sits.

Where to go from here

You now have the four actions that cover most day to day moderation, with permission checks, a warning store and a log of everything. A few sensible next steps: move warnings into SQLite once the JSON file feels heavy, add a /warnings command so staff can read a user's history, and auto time-out anyone who hits a warning threshold. When you're ready to keep it online around the clock, run it on a small VPS or a managed bot host like the one we offer at Bytte.cloud so it restarts on its own and stays up while you sleep. Keep the token secret, keep the bot's role high enough, and test new commands in a private server before you let them loose.

Common questions

What intents and permissions does a moderation bot need?

Turn on the Server Members intent in the Developer Portal, and grant the bot Kick Members, Ban Members and Moderate Members, plus View Channels and Send Messages for the log.

Why can't my bot ban an admin?

A bot can only action a member whose highest role sits below the bot's own highest role. Drag the bot's role up so it ranks above the people it manages. The server owner can never be banned.

Should I use discord.py or discord.js?

Pick the language you already know. discord.py is Python and discord.js is JavaScript. Both can do kick, ban, timeout and warn the same way, so neither has a real advantage for a moderation bot.

How do I mute someone with the bot?

Use Discord's built in timeout, which sets communication disabled until a future time. The member can't talk or join voice until it expires. The maximum length is 28 days and no muted role is needed.

Where should I store warnings?

A JSON file keyed by user id is fine for a small bot. Once it gets busy or you run multiple processes, move warnings into SQLite so concurrent reads and writes stay reliable.

TR
Tom Reyes
Support Engineer at Bytte.cloud

Part of the Bytte.cloud team. We run game servers, bots and websites for a living, and we write these guides from what we see day to day in support and on our own servers.

Want to try this on real hardware?

Bytte.cloud has free plans for game servers, bots and websites. No credit card, set up in seconds.

Start for free See the plans