cm-discord-bot-builder

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".

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cm-discord-bot-builder" with this command: npx skills add charlie-morrison/discord-bot-builder

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 TypeDescriptionIntents NeededLibrary Sweet Spot
Slash-command onlyAll interactions go through /commands. No message reading.Default (no privileged)Any library; cheapest hosting
Message-listenerReacts to plain messages (auto-mod, leveling, keyword triggers)MESSAGE_CONTENT (privileged)discord.py / discord.js
HybridSlash commands + selective message handlingMESSAGE_CONTENT only if neededdiscord.py / discord.js
Voice botMusic, TTS, recordingGUILD_VOICE_STATES + MESSAGE_CONTENTdiscord.js + lavalink, or Serenity
Mod / auditBans, mutes, audit logsGUILDS, GUILD_MEMBERS (privileged), GUILD_MODERATIONdiscord.py
Welcome / leaveOnboarding, role-on-joinGUILD_MEMBERS (privileged)Any
Presence-awareStatus-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

ComponentUse CaseLifetime
ButtonsConfirm/cancel, paginate, role assignment15 min interaction token, but custom_id persists indefinitely
Select menusMulti-choice (string, user, role, channel, mentionable)Same
ModalsForms (max 5 input fields, can chain on submit)Triggered from command or button only
AutocompleteLive search inside slash command args3-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.

StoreUse ForWhen to Pick
SQLite + aiosqliteSingle-process bot, < 50 guilds, < 1M rowsDefault for hobbyist bots; zero ops
PostgresMulti-process, sharded, multi-server, 50+ guildsProduction bots with growth
RedisHot ratelimits, command cooldowns, presence cache, leaderboardsAlways paired with primary store, never alone
JSON fileStatic config (welcome message, role IDs)Read-mostly; reload on SIGHUP
In-memory dictPer-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.

IntentPrivileged?Cost
GUILDSNoRequired for almost everything
GUILD_MESSAGESNoYou receive message events but NO content
MESSAGE_CONTENTYESRequired to read message.content. Verification needed at 100 guilds.
GUILD_MEMBERSYESMember join/leave/update events. Verification at 100 guilds.
GUILD_PRESENCESYESOnline/offline status. Hardest to get approved. Expensive event volume.
GUILD_VOICE_STATESNoVoice channel join/leave events
DIRECT_MESSAGESNoDM events
GUILD_MODERATIONNoBan/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

HostCostUptimeLogsBest For
VPS (Hetzner/DO)$4-6/mo99.9%Full systemd journalsLong-term, multi-bot, custom infra
Railway$5/mo + usage99.9%Web UI, last 7 daysQuick deploys, hobby projects
Fly.ioFree tier viable, $2-5/mo realistic99.9%fly logs, GrafanaGlobal edge, multi-region
Replit (Reserved VM)$7/mo99%Web UIBeginners, prototyping
Pterodactyl panelWhatever VPS hosts itDependsBuilt-inGame-server-style bot management
Heroku$7/mo+99.9%heroku logsAvoid — pricier than alternatives, no free tier
Lambda / Cloud RunCheapHighCloudWatchSlash-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

LayerToolWhat It Covers
Unitpytest / vitestPure logic (parsing, business rules), no Discord client
Mock clientdpytest (discord.py) / mock discord.js RESTCommand handlers without hitting Discord
Sandbox guildA throwaway server with the botReal interactions, real permissions
Fixture replaySave raw gateway events as JSON, replay through handlerReproduce production bugs
Smoke test in CIRun bot for 30s against a private guild, run a /health command, exit 0Catch 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

FeatureApproachLibrary Hints
Reaction rolesPersistent message + on_raw_reaction_add listener, or button-based (preferred — no MESSAGE_CONTENT intent)discord.py View with persistent custom_id
Ticket systemSlash command -> modal -> private thread + DB row (see worked example below)discord.py Thread API
Mod logListen on on_member_ban, on_member_remove, audit log lookup, post to channelRequires GUILD_MODERATION
MusicLavalink (Java daemon) + wavelink (py) / lavacord (js) — never decode in-processdiscord.js + lavalink is the sweet spot
LevelingXP per message (with cooldown), Postgres aggregate, optional rank card via Pillow/canvasNeeds MESSAGE_CONTENT for content-aware XP, otherwise just count messages
Captcha / verifyButton -> modal with image captcha (or hcaptcha-on-website-with-OAuth flow)Avoid math-question captchas, bots solve them trivially
Welcomeon_member_join + embed in welcome channelNeeds GUILD_MEMBERS privileged
Auto-modDiscord native AutoMod API for keyword/spam/mention rulesNative AutoMod beats custom in 90% of cases

Step 12: Security Pitfalls

RiskPrevention
Token leak in git.env in .gitignore, scan with git-secrets, rotate immediately if pushed
Token in logsNever log full request.headers. discord.py masks tokens; custom HTTP code may not.
Token in error reportsSentry: before_send hook to scrub Authorization headers
Command spoofingAll slash commands are signed by Discord — trust interaction.user, never interaction.data user fields
Privilege escalationCheck interaction.user.guild_permissions server-side; client-side default_permissions is a UX hint, not a gate
Raid preventionRate-limit on_member_join, auto-kick accounts < 1 day old during a raid, use Discord's native raid mode
DM-based attacksNever trust DM content from unverified users; sanitize before posting to a server channel
SQL injection in commandsUse 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 leaksWebhook 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 rolesCarl-bot, YAGPDB
Music (Spotify/YouTube)Hydra, Jockie, or any free music bot — building one means lavalink ops
Leveling / XPMEE6, Arcane, Tatsu
TicketsTicket Tool, TicketsBot
Anti-raid / verificationWick, Beemo, Discord native verification
PollsDiscord native /poll (built-in)
Logging / auditLogger, 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

Cross-Platform Memory Bridge

Injects recent conversations from Telegram and Discord into the OpenClaw gateway session context. Enables the agent to remember and reference cross-platform...

Registry SourceRecently Updated
1120Profile unavailable
Security

AI Code Review

Provides detailed, prioritized code review feedback on security, performance, correctness, and maintainability issues for multiple major programming languages.

Registry SourceRecently Updated
1611Profile unavailable
Coding

office secretary

A digital administrative assistant for Microsoft 365 (Outlook & OneDrive).

Registry SourceRecently Updated
1.3K0Profile unavailable
Coding

InStreet gomoku AI

InStreet五子棋AI。在InStreet桌游室进行五子棋对局时,自动计算最佳落子并提交。支持威胁检测,优先防守对手的活三/冲四。

Registry SourceRecently Updated
2590Profile unavailable