360 Secure SkillHub
360 安全技能中心

Discord Bot Builder

面向 Discord 机器人开发,支持 discord.py、discord.js 与 Serenity(Rust)三种技术栈:按机器人类型给出 gateway intents 选择,生成项目骨架、斜杠命令、按钮/模态框等交互组件,并覆盖持久化、限流处理、托管选型、日志可观测性、测试与安全防护建议。

charlie-morrison
charlie-morrison
热度 27.88
最近更新 2026-05-10 17:59:32
分类:开发依赖:代码执行 / 第三方 API / 读写文件应用场景:代码生成 / API 集成 / 调试排错 / DevOps

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add discord-bot-builder
Metadata
name
cm-discord-bot-builder
description
Scaffold, deploy, and maintain Discord bots across discord.py, discord.js, and Serenity (Rust). Generates project skeletons, registers slash commands and subcommand groups, wires up message components (buttons, select menus), modals, autocomplete, threads, and voice. Advises on gateway intents, persistent state (sqlite/postgres/redis), rate limit handling, hosting (VPS, Railway, Fly.io, Replit, Pterodactyl), observability (Sentry, structured logs, error channel), testing, and security (token leaks, command spoofing, raid prevention). Use when asked to build a Discord bot, add a slash command, create a ticket bot, reaction roles, mod log, leveling, music bot, captcha, or migrate a bot between libraries. Triggers on "discord bot", "discord.py", "discord.js", "serenity rust", "slash command", "interaction", "discord modal", "ticket bot", "reaction roles", "discord intents", "gateway intent", "discord.py cog", "bot rate limit", "discord hosting", "MEE6 alternative", "raid prevention".
metadata
{"tags":["discord","discord-bot","discord.py","discord.js","serenity","rust","python","javascript","slash-commands","interactions","gateway","community-ops","bot-development"]}

Discord Bot Builder

Scaffold, ship, and maintain Discord bots with the right library, the right intents, the right hosting, and the right observability — without the usual footguns (token leaks, missing intents, rate-limit storms, raid vulnerabilities). Covers discord.py, discord.js, and Serenity (Rust). Targets community ops teams, server admins automating moderation, and hobbyist creators building feature bots.

Usage

Invoke this skill when you need a Discord bot built, extended, or rescued.

Basic invocation:

Build me a Discord ticket bot with a /ticket command that opens a private thread Add reaction roles to my discord.js bot — react with an emoji, get a role Migrate my discord.py 1.7 bot to 2.x with slash commands My bot is getting rate-limited, help me add bucketing and retry

With context:

Here's my current bot.py, add a moderation log channel for bans/kicks/timeouts I need a Rust bot with Serenity that handles 500 guilds and tracks message counts My bot worked locally but Railway deploy crashes — read the logs and fix it Pick a host: I want sub-$10/mo, 99% uptime, easy log access

The agent decides on architecture, scaffolds the project, writes commands and handlers, wires persistence, and gives you a deploy plan.

How It Works

Step 1: Bot Type Decision

Before any code, the agent decides what kind of bot this is. The decision drives intents, hosting, and library choice.

Bot Type Description Intents Needed Library Sweet Spot
Slash-command only All interactions go through /commands. No message reading. Default (no privileged) Any library; cheapest hosting
Message-listener Reacts to plain messages (auto-mod, leveling, keyword triggers) MESSAGE_CONTENT (privileged) discord.py / discord.js
Hybrid Slash commands + selective message handling MESSAGE_CONTENT only if needed discord.py / discord.js
Voice bot Music, TTS, recording GUILD_VOICE_STATES + MESSAGE_CONTENT discord.js + lavalink, or Serenity
Mod / audit Bans, mutes, audit logs GUILDS, GUILD_MEMBERS (privileged), GUILD_MODERATION discord.py
Welcome / leave Onboarding, role-on-join GUILD_MEMBERS (privileged) Any
Presence-aware Status-based features (rare, expensive) GUILD_PRESENCES (privileged, hard to verify) Avoid unless essential

Decision flow:

Does the bot need to read message content?
  NO  -> Slash-command-only. Skip MESSAGE_CONTENT intent. Easy verification at 75 guilds.
  YES -> Will it scale past 100 guilds?
         NO  -> MESSAGE_CONTENT works without verification (under 100 guilds).
         YES -> Apply for verification + MESSAGE_CONTENT intent approval (Discord may reject).
                Consider a slash-command rewrite to avoid the privileged intent entirely.

Step 2: Project Scaffolding

The agent generates a project skeleton matched to the chosen library.

discord.py (Python 3.11+, discord.py 2.4+):

# bot.py
import os
import logging
import discord
from discord.ext import commands
from dotenv import load_dotenv

load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")

intents = discord.Intents.default()
intents.message_content = False  # Flip to True only if you need it

class MyBot(commands.Bot):
    def __init__(self):
        super().__init__(command_prefix="!", intents=intents)

    async def setup_hook(self):
        await self.load_extension("cogs.tickets")
        await self.load_extension("cogs.moderation")
        await self.tree.sync()  # Sync slash commands globally (~1hr propagation)

    async def on_ready(self):
        logging.info(f"Logged in as {self.user} ({self.user.id})")

bot = MyBot()
bot.run(os.environ["DISCORD_TOKEN"])
project/
  bot.py
  cogs/
    tickets.py
    moderation.py
  db/
    schema.sql
  .env             # DISCORD_TOKEN=... (gitignored)
  requirements.txt # discord.py>=2.4, python-dotenv, aiosqlite
  Dockerfile
  README.md

discord.js (Node 20+, discord.js v14+):

// index.js
import 'dotenv/config';
import { Client, GatewayIntentBits, Collection, Events } from 'discord.js';
import { readdirSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';

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

const __dirname = dirname(fileURLToPath(import.meta.url));
for (const file of readdirSync(join(__dirname, 'commands'))) {
  const mod = await import(pathToFileURL(join(__dirname, 'commands', file)).href);
  client.commands.set(mod.data.name, mod);
}

client.on(Events.InteractionCreate, async (i) => {
  if (!i.isChatInputCommand()) return;
  const cmd = client.commands.get(i.commandName);
  if (!cmd) return;
  try { await cmd.execute(i); }
  catch (e) { console.error(e); await i.reply({ content: 'Error.', ephemeral: true }); }
});

client.login(process.env.DISCORD_TOKEN);
project/
  index.js
  commands/
    ticket.js
    ban.js
  deploy-commands.js   # Registers slash commands via REST
  package.json         # type: module, discord.js@14
  .env

Serenity (Rust, async, Tokio):

// src/main.rs
use serenity::all::{Client, Context, EventHandler, GatewayIntents, Interaction, Ready};
use serenity::async_trait;
use std::env;

struct Handler;

#[async_trait]
impl EventHandler for Handler {
    async fn ready(&self, _ctx: Context, ready: Ready) {
        println!("Logged in as {}", ready.user.name);
    }

    async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
        if let Interaction::Command(cmd) = interaction {
            match cmd.data.name.as_str() {
                "ping" => commands::ping::run(&ctx, &cmd).await,
                _ => {}
            }
        }
    }
}

#[tokio::main]
async fn main() {
    let token = env::var("DISCORD_TOKEN").expect("DISCORD_TOKEN");
    let intents = GatewayIntents::GUILDS;
    let mut client = Client::builder(&token, intents).event_handler(Handler).await.unwrap();
    if let Err(e) = client.start().await { eprintln!("{e}"); }
}
project/
  Cargo.toml          # serenity, tokio, sqlx, tracing
  src/
    main.rs
    commands/
      ping.rs
      ticket.rs

Step 3: Command Registration

discord.py — slash command with subcommand group:

from discord import app_commands
import discord

class Tickets(commands.GroupCog, name="ticket"):
    @app_commands.command(name="open", description="Open a support ticket")
    @app_commands.describe(reason="Why you need help")
    async def open(self, interaction: discord.Interaction, reason: str):
        await interaction.response.send_message(f"Ticket opened: {reason}", ephemeral=True)

    @app_commands.command(name="close", description="Close your ticket")
    @app_commands.default_permissions(manage_threads=True)  # Permission gate
    async def close(self, interaction: discord.Interaction):
        await interaction.response.send_message("Closed.", ephemeral=True)

async def setup(bot): await bot.add_cog(Tickets(bot))

Permission patterns:

  • @app_commands.default_permissions(...) — server-side default, admins can override per-role in Server Settings -> Integrations
  • @app_commands.guild_only() — block DMs
  • Manual check via interaction.user.guild_permissions.ban_members for runtime decisions

Sync strategy:

  • bot.tree.sync() global — propagates over ~1 hour, use for production
  • bot.tree.sync(guild=discord.Object(id=GUILD_ID)) — instant, use for development

Step 4: Interaction Patterns

Component Use Case Lifetime
Buttons Confirm/cancel, paginate, role assignment 15 min interaction token, but custom_id persists indefinitely
Select menus Multi-choice (string, user, role, channel, mentionable) Same
Modals Forms (max 5 input fields, can chain on submit) Triggered from command or button only
Autocomplete Live search inside slash command args 3-second response window

Modal example (discord.py):

class TicketModal(discord.ui.Modal, title="Open a ticket"):
    subject = discord.ui.TextInput(label="Subject", max_length=80)
    details = discord.ui.TextInput(label="Details", style=discord.TextStyle.paragraph, max_length=1500)

    async def on_submit(self, interaction: discord.Interaction):
        thread = await interaction.channel.create_thread(
            name=f"ticket-{interaction.user.name}",
            type=discord.ChannelType.private_thread,
        )
        await thread.send(f"{interaction.user.mention} **{self.subject}**\n{self.details}")
        await interaction.response.send_message(f"Opened {thread.mention}", ephemeral=True)

Persistent custom_id pattern (survives bot restarts):

class RoleButton(discord.ui.View):
    def __init__(self): super().__init__(timeout=None)  # timeout=None = persistent

    @discord.ui.button(label="Get role", custom_id="rolebtn:gamer")
    async def get_role(self, interaction, button): ...

# In setup_hook:
bot.add_view(RoleButton())  # Re-register on startup so the custom_id keeps working

Step 5: Persistent State

Pick by access pattern, not by hype.

Store Use For When to Pick
SQLite + aiosqlite Single-process bot, < 50 guilds, < 1M rows Default for hobbyist bots; zero ops
Postgres Multi-process, sharded, multi-server, 50+ guilds Production bots with growth
Redis Hot ratelimits, command cooldowns, presence cache, leaderboards Always paired with primary store, never alone
JSON file Static config (welcome message, role IDs) Read-mostly; reload on SIGHUP
In-memory dict Per-runtime state (active tickets, music queue) Volatile by design; never put user data here

Sharding hint: if you cross 2,500 guilds Discord requires sharding. Each shard is a separate gateway connection. Postgres + Redis is the only sane backing store from that point.

# aiosqlite skeleton (discord.py cog)
import aiosqlite

class Tickets(commands.Cog):
    async def cog_load(self):
        self.db = await aiosqlite.connect("tickets.db")
        await self.db.execute("""CREATE TABLE IF NOT EXISTS tickets (
            id INTEGER PRIMARY KEY, user_id INTEGER, thread_id INTEGER,
            subject TEXT, opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )""")
        await self.db.commit()

Step 6: Gateway Intents

Intents tell Discord which events to send you. Wrong intents = silent failures.

Intent Privileged? Cost
GUILDS No Required for almost everything
GUILD_MESSAGES No You receive message events but NO content
MESSAGE_CONTENT YES Required to read message.content. Verification needed at 100 guilds.
GUILD_MEMBERS YES Member join/leave/update events. Verification at 100 guilds.
GUILD_PRESENCES YES Online/offline status. Hardest to get approved. Expensive event volume.
GUILD_VOICE_STATES No Voice channel join/leave events
DIRECT_MESSAGES No DM events
GUILD_MODERATION No Ban/unban/audit-log events

Privacy implications:

  • MESSAGE_CONTENT reads every message in every channel the bot can see — Discord scrutinizes this. Justify it in your verification application or don't ask.
  • GUILD_PRESENCES at scale = millions of presence updates per minute. Most bots that ask for it don't actually need it (use member.activity only when needed).
  • Default-off everything privileged; flip on what's load-bearing.

Step 7: Rate Limit Handling

Discord rate-limits per-route, per-major-parameter (channel/guild/webhook), and globally (50 req/sec per bot).

Library defaults (good starting point):

  • discord.py auto-handles 429s with X-RateLimit-Reset-After
  • discord.js v14 has a built-in REST queue with retries
  • Serenity's Http does the same

When to add custom logic:

# Bulk operations: add jitter and bucketing
import asyncio, random

async def mass_dm(users, message):
    for user in users:
        try:
            await user.send(message)
        except discord.Forbidden:
            pass
        except discord.HTTPException as e:
            if e.status == 429:
                await asyncio.sleep(e.response.headers.get("Retry-After", 5))
        await asyncio.sleep(1.0 + random.random() * 0.5)  # 1-1.5s jitter

Rules:

  • Never asyncio.gather 100 API calls — bucket them
  • Add jitter on bulk loops to avoid synchronized retry storms
  • Cache reads where possible (use bot.get_* before bot.fetch_*)
  • Webhook routes have their own bucket — useful for high-volume logging

Step 8: Hosting Choice Tree

Host Cost Uptime Logs Best For
VPS (Hetzner/DO) $4-6/mo 99.9% Full systemd journals Long-term, multi-bot, custom infra
Railway $5/mo + usage 99.9% Web UI, last 7 days Quick deploys, hobby projects
Fly.io Free tier viable, $2-5/mo realistic 99.9% fly logs, Grafana Global edge, multi-region
Replit (Reserved VM) $7/mo 99% Web UI Beginners, prototyping
Pterodactyl panel Whatever VPS hosts it Depends Built-in Game-server-style bot management
Heroku $7/mo+ 99.9% heroku logs Avoid — pricier than alternatives, no free tier
Lambda / Cloud Run Cheap High CloudWatch Slash-command-only via interactions endpoint URL

Decision tree:

Bot uses gateway (most do)?
  YES -> Long-running process. Pick VPS, Railway, Fly, or Replit.
         < 100 guilds, want easy:    Railway or Replit
         Want cheapest stable:        Hetzner CX11 ($4/mo)
         Multi-region / many shards:  Fly.io
  NO (slash-only via HTTP endpoint) -> Cloud Run / Lambda / Cloudflare Workers.

E200HA-class hosting note: a 4GB-RAM small bot can run on a $4 Hetzner box; for 500+ guilds plan 1GB RAM per 1000 guilds plus Postgres.

Step 9: Logging + Observability

Minimum viable:

# Structured JSON logs to stdout, then ship to wherever (Loki, Datadog, plain file)
import logging, json, sys

class JsonFormatter(logging.Formatter):
    def format(self, r):
        return json.dumps({
            "ts": self.formatTime(r), "level": r.levelname,
            "msg": r.getMessage(), "logger": r.name,
            **(getattr(r, "extra", {}) or {})
        })

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logging.getLogger().addHandler(handler)

Sentry for unhandled errors:

import sentry_sdk
sentry_sdk.init(dsn=os.environ["SENTRY_DSN"], traces_sample_rate=0.1)
# discord.py raises in command handlers — Sentry SDK auto-captures via global exception hook

Error notifications to a Discord channel (poor man's alerting, free):

@bot.event
async def on_command_error(ctx, error):
    log_channel = bot.get_channel(LOG_CHANNEL_ID)
    await log_channel.send(f"```{type(error).__name__}: {error}```")
    raise error  # Re-raise for Sentry

What to log:

  • Every command invocation: user_id, guild_id, command, latency_ms
  • Every gateway disconnect/resume
  • Every 4xx/5xx from REST API
  • Token rotation events
  • Rate-limit hits (count per route)

Step 10: Testing Strategy

Layer Tool What It Covers
Unit pytest / vitest Pure logic (parsing, business rules), no Discord client
Mock client dpytest (discord.py) / mock discord.js REST Command handlers without hitting Discord
Sandbox guild A throwaway server with the bot Real interactions, real permissions
Fixture replay Save raw gateway events as JSON, replay through handler Reproduce production bugs
Smoke test in CI Run bot for 30s against a private guild, run a /health command, exit 0 Catch import/auth/intent breakage before deploy

Sandbox guild setup:

  • Create bot-dev server, bot has Admin
  • Use guild-scoped command sync for instant updates: await tree.sync(guild=GUILD)
  • Keep production token and dev token in separate .env.dev / .env.prod

Step 11: Common Features

Feature Approach Library Hints
Reaction roles Persistent message + on_raw_reaction_add listener, or button-based (preferred — no MESSAGE_CONTENT intent) discord.py View with persistent custom_id
Ticket system Slash command -> modal -> private thread + DB row (see worked example below) discord.py Thread API
Mod log Listen on on_member_ban, on_member_remove, audit log lookup, post to channel Requires GUILD_MODERATION
Music Lavalink (Java daemon) + wavelink (py) / lavacord (js) — never decode in-process discord.js + lavalink is the sweet spot
Leveling XP per message (with cooldown), Postgres aggregate, optional rank card via Pillow/canvas Needs MESSAGE_CONTENT for content-aware XP, otherwise just count messages
Captcha / verify Button -> modal with image captcha (or hcaptcha-on-website-with-OAuth flow) Avoid math-question captchas, bots solve them trivially
Welcome on_member_join + embed in welcome channel Needs GUILD_MEMBERS privileged
Auto-mod Discord native AutoMod API for keyword/spam/mention rules Native AutoMod beats custom in 90% of cases

Step 12: Security Pitfalls

Risk Prevention
Token leak in git .env in .gitignore, scan with git-secrets, rotate immediately if pushed
Token in logs Never log full request.headers. discord.py masks tokens; custom HTTP code may not.
Token in error reports Sentry: before_send hook to scrub Authorization headers
Command spoofing All slash commands are signed by Discord — trust interaction.user, never interaction.data user fields
Privilege escalation Check interaction.user.guild_permissions server-side; client-side default_permissions is a UX hint, not a gate
Raid prevention Rate-limit on_member_join, auto-kick accounts < 1 day old during a raid, use Discord's native raid mode
DM-based attacks Never trust DM content from unverified users; sanitize before posting to a server channel
SQL injection in commands Use parameterized queries always, even for "internal" admin commands
Prompt injection (AI bots) Strip Discord markdown from user input before sending to LLM; never put user input in system prompts
Webhook URL leaks Webhook URLs are bearer credentials — rotate if leaked, use them server-side only

Worked Example: discord.py Ticket Bot

A minimal but real ticket bot. /ticket opens a modal, modal submission creates a private thread tagged to the user, support staff are auto-added.

# bot.py
import os, logging, aiosqlite
import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv

load_dotenv()
logging.basicConfig(level=logging.INFO)
SUPPORT_ROLE_ID = int(os.environ["SUPPORT_ROLE_ID"])

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

class TicketModal(discord.ui.Modal, title="Open a support ticket"):
    subject = discord.ui.TextInput(label="Subject", max_length=80, required=True)
    details = discord.ui.TextInput(
        label="Details", style=discord.TextStyle.paragraph, max_length=1500, required=True,
    )

    async def on_submit(self, interaction: discord.Interaction):
        guild = interaction.guild
        thread = await interaction.channel.create_thread(
            name=f"ticket-{interaction.user.name}-{self.subject.value[:30]}",
            type=discord.ChannelType.private_thread,
            invitable=False,
        )
        await thread.add_user(interaction.user)
        support_role = guild.get_role(SUPPORT_ROLE_ID)
        if support_role:
            for m in support_role.members:
                try: await thread.add_user(m)
                except discord.HTTPException: pass

        embed = discord.Embed(title=self.subject.value, description=self.details.value, color=0x5865F2)
        embed.set_author(name=str(interaction.user), icon_url=interaction.user.display_avatar.url)
        await thread.send(embed=embed, view=CloseButton())

        async with aiosqlite.connect("tickets.db") as db:
            await db.execute(
                "INSERT INTO tickets (user_id, thread_id, subject) VALUES (?, ?, ?)",
                (interaction.user.id, thread.id, self.subject.value),
            )
            await db.commit()

        await interaction.response.send_message(
            f"Ticket opened: {thread.mention}", ephemeral=True,
        )

class CloseButton(discord.ui.View):
    def __init__(self): super().__init__(timeout=None)

    @discord.ui.button(label="Close ticket", style=discord.ButtonStyle.danger, custom_id="ticket:close")
    async def close(self, interaction: discord.Interaction, button: discord.ui.Button):
        if not isinstance(interaction.channel, discord.Thread):
            return await interaction.response.send_message("Not a ticket.", ephemeral=True)
        await interaction.response.send_message("Closing in 5s...", ephemeral=False)
        await interaction.channel.edit(archived=True, locked=True)

@bot.tree.command(name="ticket", description="Open a support ticket")
async def ticket(interaction: discord.Interaction):
    await interaction.response.send_modal(TicketModal())

@bot.event
async def setup_hook():
    async with aiosqlite.connect("tickets.db") as db:
        await db.execute("""CREATE TABLE IF NOT EXISTS tickets (
            id INTEGER PRIMARY KEY, user_id INTEGER, thread_id INTEGER,
            subject TEXT, opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )""")
        await db.commit()
    bot.add_view(CloseButton())  # Persist button across restarts
    await bot.tree.sync()

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

What this demonstrates:

  • Slash command -> modal -> thread creation in one flow
  • Persistent view (bot.add_view) so the Close button survives restarts
  • Private thread with explicit invite list (no MESSAGE_CONTENT needed)
  • SQLite persistence via aiosqlite
  • Ephemeral response (only the user sees the confirmation)
  • Embed with author attribution
  • Defensive HTTP error handling on bulk add

To run:

pip install discord.py aiosqlite python-dotenv
echo 'DISCORD_TOKEN=...' > .env
echo 'SUPPORT_ROLE_ID=123456789' >> .env
python bot.py

Output

The agent produces:

  • Bot type decision with intent recommendations and verification implications
  • Project skeleton in your chosen library (discord.py / discord.js / Serenity)
  • Command and handler code matched to your features
  • Persistence layer with schema and access pattern
  • Hosting recommendation with concrete cost and deploy steps
  • Observability setup: structured logs, Sentry init, error-to-channel hook
  • Test scaffold: unit tests, mock client setup, sandbox-guild deploy script
  • Security checklist: token handling, permission gates, raid mitigations
  • Migration path if rewriting from an old library version

Common Scenarios

"Build me a bot that does X"

The agent picks the library based on your requirements (Python for ML/data, JS for ecosystem breadth, Rust for scale), scaffolds the project, writes the feature, and gives you a deploy plan.

"My bot is rate-limited, getting 429s in production"

The agent inspects your loop patterns, adds bucketing + jitter, recommends Redis-backed cooldowns if needed, and configures library-level retry settings.

"I need to migrate from discord.py 1.7 to 2.x" (or v13 -> v14 discord.js)

The agent maps deprecated APIs to new ones (intents, slash commands, Client.run -> async commands.Bot, Embed builder changes), updates handler signatures, and provides a working migration patch.

"My bot keeps crashing on Railway / Fly / Replit"

The agent reads the deployment logs, identifies the cause (missing intent, missing env var, wrong Python version, missing FFmpeg for voice, stale token), and applies the fix.

"Build me a slash-command-only bot for Cloud Run"

The agent uses Discord's HTTP interactions endpoint URL pattern (PING/PONG verification + Ed25519 signature check), avoiding the gateway entirely. Stateless, scales to zero.

Tips for Best Results

  • Tell the agent your target guild count — it changes library, hosting, and persistence choice
  • Mention if you need privileged intents — verification adds 1-2 weeks of lead time
  • Share existing code if migrating; the agent diffs old patterns against new APIs
  • For voice bots, specify whether you want music streaming (lavalink) or recording/TTS (different stacks)
  • Provide your deploy target up front — Railway and Fly have different Procfile / fly.toml patterns
  • If you have a moderation use case, ask whether Discord's native AutoMod covers it before building custom

When NOT to use

Don't build a bot when an existing one already does the job well. Pay-as-you-go bots beat months of unpaid maintenance.

If you need... Use this instead of building
Generic moderation (warns, mutes, automod) MEE6, Dyno, Carl-bot
Reaction roles Carl-bot, YAGPDB
Music (Spotify/YouTube) Hydra, Jockie, or any free music bot — building one means lavalink ops
Leveling / XP MEE6, Arcane, Tatsu
Tickets Ticket Tool, TicketsBot
Anti-raid / verification Wick, Beemo, Discord native verification
Polls Discord native /poll (built-in)
Logging / audit Logger, GearBot

Build your own only when:

  • The feature is novel and no existing bot does it
  • You need tight integration with your own backend (your game, your SaaS, your API)
  • You're deeply customizing UX and the off-the-shelf bot fights you
  • You're learning bot development and the goal is the journey, not the result
  • The off-the-shelf bot has a paywall on the exact feature you need and your time is worth less than $5/mo

If MEE6 or Dyno already does 90% of what you want, configure them and ship. A working bot you didn't write beats a half-finished bot you did.