Antigravity-awesome-skills discord-bot-architect
Specialized skill for building production-ready Discord bots.
git clone https://github.com/sickn33/antigravity-awesome-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/sickn33/antigravity-awesome-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/antigravity-awesome-skills-claude/skills/discord-bot-architect" ~/.claude/skills/sickn33-antigravity-awesome-skills-discord-bot-architect && rm -rf "$T"
plugins/antigravity-awesome-skills-claude/skills/discord-bot-architect/SKILL.md- references .env files
Discord Bot Architect
Specialized skill for building production-ready Discord bots. Covers Discord.js (JavaScript) and Pycord (Python), gateway intents, slash commands, interactive components, rate limiting, and sharding.
Principles
- Slash commands over message parsing (Message Content Intent deprecated)
- Acknowledge interactions within 3 seconds, always
- Request only required intents (minimize privileged intents)
- Handle rate limits gracefully with exponential backoff
- Plan for sharding from the start (required at 2500+ guilds)
- Use components (buttons, selects, modals) for rich UX
- Test with guild commands first, deploy global when ready
Patterns
Discord.js v14 Foundation
Modern Discord bot setup with Discord.js v14 and slash commands
When to use: Building Discord bots with JavaScript/TypeScript,Need full gateway connection with events,Building bots with complex interactions
// src/index.js const { Client, Collection, GatewayIntentBits, Events } = require('discord.js'); const fs = require('node:fs'); const path = require('node:path'); require('dotenv').config(); // Create client with minimal required intents const client = new Client({ intents: [ GatewayIntentBits.Guilds, // Add only what you need: // GatewayIntentBits.GuildMessages, // GatewayIntentBits.MessageContent, // PRIVILEGED - avoid if possible ] }); // Load commands client.commands = new Collection(); const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js')); for (const file of commandFiles) { const filePath = path.join(commandsPath, file); const command = require(filePath); if ('data' in command && 'execute' in command) { client.commands.set(command.data.name, command); } } // Load events const eventsPath = path.join(__dirname, 'events'); const eventFiles = fs.readdirSync(eventsPath).filter(f => f.endsWith('.js')); for (const file of eventFiles) { const filePath = path.join(eventsPath, file); const event = require(filePath); if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { client.on(event.name, (...args) => event.execute(...args)); } } client.login(process.env.DISCORD_TOKEN);
// src/commands/ping.js const { SlashCommandBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('ping') .setDescription('Replies with Pong!'), async execute(interaction) { const sent = await interaction.reply({ content: 'Pinging...', fetchReply: true }); const latency = sent.createdTimestamp - interaction.createdTimestamp; await interaction.editReply(`Pong! Latency: ${latency}ms`); } };
// src/events/interactionCreate.js const { Events } = require('discord.js'); module.exports = { name: Events.InteractionCreate, async execute(interaction) { if (!interaction.isChatInputCommand()) return; const command = interaction.client.commands.get(interaction.commandName); if (!command) { console.error(`No command matching ${interaction.commandName}`); return; } try { await command.execute(interaction); } catch (error) { console.error(error); const reply = { content: 'There was an error executing this command!', ephemeral: true }; if (interaction.replied || interaction.deferred) { await interaction.followUp(reply); } else { await interaction.reply(reply); } } } };
// src/deploy-commands.js const { REST, Routes } = require('discord.js'); const fs = require('node:fs'); const path = require('node:path'); require('dotenv').config(); const commands = []; const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js')); for (const file of commandFiles) { const command = require(path.join(commandsPath, file)); commands.push(command.data.toJSON()); } const rest = new REST().setToken(process.env.DISCORD_TOKEN); (async () => { try { console.log(`Refreshing ${commands.length} commands...`); // Guild commands (instant, for testing) // const data = await rest.put( // Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), // { body: commands } // ); // Global commands (can take up to 1 hour to propagate) const data = await rest.put( Routes.applicationCommands(process.env.CLIENT_ID), { body: commands } ); console.log(`Successfully registered ${data.length} commands`); } catch (error) { console.error(error); } })();
Structure
discord-bot/ ├── src/ │ ├── index.js # Main entry point │ ├── deploy-commands.js # Command registration script │ ├── commands/ # Slash command handlers │ │ └── ping.js │ └── events/ # Event handlers │ ├── ready.js │ └── interactionCreate.js ├── .env └── package.json
Pycord Bot Foundation
Discord bot with Pycord (Python) and application commands
When to use: Building Discord bots with Python,Prefer async/await patterns,Need good slash command support
# main.py import os import discord from discord.ext import commands from dotenv import load_dotenv load_dotenv() # Configure intents - only enable what you need intents = discord.Intents.default() # intents.message_content = True # PRIVILEGED - avoid if possible # intents.members = True # PRIVILEGED bot = commands.Bot( command_prefix="!", # Legacy, prefer slash commands intents=intents ) @bot.event async def on_ready(): print(f"Logged in as {bot.user}") # Sync commands (do this carefully - see sharp edges) # await bot.sync_commands() # Slash command @bot.slash_command(name="ping", description="Check bot latency") async def ping(ctx: discord.ApplicationContext): latency = round(bot.latency * 1000) await ctx.respond(f"Pong! Latency: {latency}ms") # Slash command with options @bot.slash_command(name="greet", description="Greet a user") async def greet( ctx: discord.ApplicationContext, user: discord.Option(discord.Member, "User to greet"), message: discord.Option(str, "Custom message", required=False) ): msg = message or "Hello!" await ctx.respond(f"{user.mention}, {msg}") # Load cogs for filename in os.listdir("./cogs"): if filename.endswith(".py"): bot.load_extension(f"cogs.{filename[:-3]}") bot.run(os.environ["DISCORD_TOKEN"])
# cogs/general.py import discord from discord.ext import commands class General(commands.Cog): def __init__(self, bot): self.bot = bot @commands.slash_command(name="info", description="Bot information") async def info(self, ctx: discord.ApplicationContext): embed = discord.Embed( title="Bot Info", description="A helpful Discord bot", color=discord.Color.blue() ) embed.add_field(name="Servers", value=len(self.bot.guilds)) embed.add_field(name="Latency", value=f"{round(self.bot.latency * 1000)}ms") await ctx.respond(embed=embed) @commands.Cog.listener() async def on_member_join(self, member: discord.Member): # Requires Members intent (PRIVILEGED) channel = member.guild.system_channel if channel: await channel.send(f"Welcome {member.mention}!") def setup(bot): bot.add_cog(General(bot))
Structure
discord-bot/ ├── main.py # Main bot file ├── cogs/ # Command groups │ └── general.py ├── .env └── requirements.txt
Interactive Components Pattern
Using buttons, select menus, and modals for rich UX
When to use: Need interactive user interfaces,Collecting user input beyond slash command options,Building menus, confirmations, or forms
// Discord.js - Buttons and Select Menus const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('menu') .setDescription('Shows an interactive menu'), async execute(interaction) { // Button row const buttonRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('confirm') .setLabel('Confirm') .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId('cancel') .setLabel('Cancel') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setLabel('Documentation') .setURL('https://discord.js.org') .setStyle(ButtonStyle.Link) // Link buttons don't emit events ); // Select menu row (one per row, takes all 5 slots) const selectRow = new ActionRowBuilder() .addComponents( new StringSelectMenuBuilder() .setCustomId('select-role') .setPlaceholder('Select a role') .setMinValues(1) .setMaxValues(3) .addOptions([ { label: 'Developer', value: 'dev', emoji: '💻' }, { label: 'Designer', value: 'design', emoji: '🎨' }, { label: 'Community', value: 'community', emoji: '🎉' } ]) ); await interaction.reply({ content: 'Choose an option:', components: [buttonRow, selectRow] }); // Collect responses const collector = interaction.channel.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60_000 // 60 seconds timeout }); collector.on('collect', async i => { if (i.customId === 'confirm') { await i.update({ content: 'Confirmed!', components: [] }); collector.stop(); } else if (i.customId === 'cancel') { await i.update({ content: 'Cancelled', components: [] }); collector.stop(); } else if (i.customId === 'select-role') { await i.update({ content: `You selected: ${i.values.join(', ')}` }); } }); } };
// Modals (forms) module.exports = { data: new SlashCommandBuilder() .setName('feedback') .setDescription('Submit feedback'), async execute(interaction) { const modal = new ModalBuilder() .setCustomId('feedback-modal') .setTitle('Submit Feedback'); const titleInput = new TextInputBuilder() .setCustomId('feedback-title') .setLabel('Title') .setStyle(TextInputStyle.Short) .setRequired(true) .setMaxLength(100); const bodyInput = new TextInputBuilder() .setCustomId('feedback-body') .setLabel('Your feedback') .setStyle(TextInputStyle.Paragraph) .setRequired(true) .setMaxLength(1000) .setPlaceholder('Describe your feedback...'); modal.addComponents( new ActionRowBuilder().addComponents(titleInput), new ActionRowBuilder().addComponents(bodyInput) ); // Show modal - MUST be first response await interaction.showModal(modal); } }; // Handle modal submission in interactionCreate if (interaction.isModalSubmit()) { if (interaction.customId === 'feedback-modal') { const title = interaction.fields.getTextInputValue('feedback-title'); const body = interaction.fields.getTextInputValue('feedback-body'); await interaction.reply({ content: `Thanks for your feedback!\n**${title}**\n${body}`, ephemeral: true }); } }
# Pycord - Buttons and Views import discord class ConfirmView(discord.ui.View): def __init__(self): super().__init__(timeout=60) self.value = None @discord.ui.button(label="Confirm", style=discord.ButtonStyle.green) async def confirm(self, button, interaction): self.value = True await interaction.response.edit_message(content="Confirmed!", view=None) self.stop() @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) async def cancel(self, button, interaction): self.value = False await interaction.response.edit_message(content="Cancelled", view=None) self.stop() @bot.slash_command(name="confirm") async def confirm_cmd(ctx: discord.ApplicationContext): view = ConfirmView() await ctx.respond("Are you sure?", view=view) await view.wait() # Wait for user interaction if view.value is None: await ctx.followup.send("Timed out") # Select Menu class RoleSelect(discord.ui.Select): def __init__(self): options = [ discord.SelectOption(label="Developer", value="dev", emoji="💻"), discord.SelectOption(label="Designer", value="design", emoji="🎨"), ] super().__init__( placeholder="Select roles...", min_values=1, max_values=2, options=options ) async def callback(self, interaction): await interaction.response.send_message( f"You selected: {', '.join(self.values)}", ephemeral=True ) class RoleView(discord.ui.View): def __init__(self): super().__init__() self.add_item(RoleSelect()) # Modal class FeedbackModal(discord.ui.Modal): def __init__(self): super().__init__(title="Submit Feedback") self.add_item(discord.ui.InputText( label="Title", style=discord.InputTextStyle.short, required=True, max_length=100 )) self.add_item(discord.ui.InputText( label="Feedback", style=discord.InputTextStyle.long, required=True, max_length=1000 )) async def callback(self, interaction): title = self.children[0].value body = self.children[1].value await interaction.response.send_message( f"Thanks!\n**{title}**\n{body}", ephemeral=True ) @bot.slash_command(name="feedback") async def feedback(ctx: discord.ApplicationContext): await ctx.send_modal(FeedbackModal())
Limits
- 5 ActionRows per message/modal
- 5 buttons per ActionRow
- 1 select menu per ActionRow (takes all 5 slots)
- 5 select menus max per message
- 25 options per select menu
- Modal must be first response (cannot defer first)
Deferred Response Pattern
Handle slow operations without timing out
When to use: Operation takes more than 3 seconds,Database queries, API calls, LLM responses,File processing or generation
// Discord.js - Deferred response module.exports = { data: new SlashCommandBuilder() .setName('slow-task') .setDescription('Performs a slow operation'), async execute(interaction) { // Defer immediately - you have 3 seconds! await interaction.deferReply(); // For ephemeral: await interaction.deferReply({ ephemeral: true }); try { // Now you have 15 minutes to complete const result = await slowDatabaseQuery(); const aiResponse = await callOpenAI(result); // Edit the deferred reply await interaction.editReply({ content: `Result: ${aiResponse}`, embeds: [resultEmbed] }); } catch (error) { await interaction.editReply({ content: 'An error occurred while processing your request.' }); } } }; // For components (buttons, select menus) collector.on('collect', async i => { await i.deferUpdate(); // Acknowledge without visual change // Or: await i.deferReply({ ephemeral: true }); const result = await slowOperation(); await i.editReply({ content: result }); });
# Pycord - Deferred response @bot.slash_command(name="slow-task") async def slow_task(ctx: discord.ApplicationContext): # Defer immediately await ctx.defer() # For ephemeral: await ctx.defer(ephemeral=True) try: result = await slow_database_query() ai_response = await call_openai(result) await ctx.followup.send(f"Result: {ai_response}") except Exception as e: await ctx.followup.send("An error occurred")
Timing
- Initial_response: 3 seconds
- Deferred_followup: 15 minutes
- Ephemeral_note: Can only be set on initial response, not changed later
Embed Builder Pattern
Rich embedded messages for professional-looking content
When to use: Displaying formatted information,Status updates, help menus, logs,Data with structure (fields, images)
const { EmbedBuilder, Colors } = require('discord.js'); // Basic embed const embed = new EmbedBuilder() .setColor(Colors.Blue) .setTitle('Bot Status') .setURL('https://example.com') .setAuthor({ name: 'Bot Name', iconURL: client.user.displayAvatarURL() }) .setDescription('Current status and statistics') .addFields( { name: 'Servers', value: `${client.guilds.cache.size}`, inline: true }, { name: 'Users', value: `${client.users.cache.size}`, inline: true }, { name: 'Uptime', value: formatUptime(), inline: true } ) .setThumbnail(client.user.displayAvatarURL()) .setImage('https://example.com/banner.png') .setTimestamp() .setFooter({ text: 'Requested by User', iconURL: interaction.user.displayAvatarURL() }); await interaction.reply({ embeds: [embed] }); // Multiple embeds (max 10) await interaction.reply({ embeds: [embed1, embed2, embed3] });
# Pycord embed = discord.Embed( title="Bot Status", description="Current status and statistics", color=discord.Color.blue(), url="https://example.com" ) embed.set_author( name="Bot Name", icon_url=bot.user.display_avatar.url ) embed.add_field(name="Servers", value=len(bot.guilds), inline=True) embed.add_field(name="Users", value=len(bot.users), inline=True) embed.set_thumbnail(url=bot.user.display_avatar.url) embed.set_image(url="https://example.com/banner.png") embed.set_footer(text="Requested by User", icon_url=ctx.author.display_avatar.url) embed.timestamp = discord.utils.utcnow() await ctx.respond(embed=embed)
Limits
- 10 embeds per message
- 6000 characters total across all embeds
- 256 characters for title
- 4096 characters for description
- 25 fields per embed
- 256 characters per field name
- 1024 characters per field value
Rate Limit Handling Pattern
Gracefully handle Discord API rate limits
When to use: High-volume operations,Bulk messaging or role assignments,Any repeated API calls
// Discord.js handles rate limits automatically, but for custom handling: const { REST } = require('discord.js'); const rest = new REST({ version: '10' }) .setToken(process.env.DISCORD_TOKEN); rest.on('rateLimited', (info) => { console.log(`Rate limited! Retry after ${info.retryAfter}ms`); console.log(`Route: ${info.route}`); console.log(`Global: ${info.global}`); }); // Queue pattern for bulk operations class RateLimitQueue { constructor() { this.queue = []; this.processing = false; this.requestsPerSecond = 40; // Safe margin below 50 } async add(operation) { return new Promise((resolve, reject) => { this.queue.push({ operation, resolve, reject }); this.process(); }); } async process() { if (this.processing || this.queue.length === 0) return; this.processing = true; while (this.queue.length > 0) { const { operation, resolve, reject } = this.queue.shift(); try { const result = await operation(); resolve(result); } catch (error) { reject(error); } // Throttle: ~40 requests per second await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond)); } this.processing = false; } } const queue = new RateLimitQueue(); // Usage: Send 200 messages without hitting rate limits for (const user of users) { await queue.add(() => user.send('Welcome!')); }
# Pycord/discord.py handles rate limits automatically # For custom handling: import asyncio from collections import deque class RateLimitQueue: def __init__(self, requests_per_second=40): self.queue = deque() self.processing = False self.delay = 1 / requests_per_second async def add(self, coro): future = asyncio.Future() self.queue.append((coro, future)) if not self.processing: asyncio.create_task(self._process()) return await future async def _process(self): self.processing = True while self.queue: coro, future = self.queue.popleft() try: result = await coro future.set_result(result) except Exception as e: future.set_exception(e) await asyncio.sleep(self.delay) self.processing = False queue = RateLimitQueue() # Usage for member in guild.members: await queue.add(member.send("Welcome!"))
Rate_limits
- Global: 50 requests per second
- Gateway: 120 requests per 60 seconds
- Specific: Messages to same channel: 5/5s, Bulk delete: 1/1s, Guild member requests: varies by guild size
Sharding Pattern
Scale bots to 2500+ servers with sharding
When to use: Bot approaching 2500 guilds (required),Want horizontal scaling,Memory optimization for large bots
// Discord.js Sharding Manager // shard.js (main entry) const { ShardingManager } = require('discord.js'); const manager = new ShardingManager('./bot.js', { token: process.env.DISCORD_TOKEN, totalShards: 'auto', // Discord determines optimal count // Or specify: totalShards: 4 }); manager.on('shardCreate', shard => { console.log(`Launched shard ${shard.id}`); shard.on('ready', () => { console.log(`Shard ${shard.id} ready`); }); shard.on('disconnect', () => { console.log(`Shard ${shard.id} disconnected`); }); }); manager.spawn(); // bot.js - Modified for sharding const { Client } = require('discord.js'); const client = new Client({ intents: [...] }); // Get shard info client.on('ready', () => { console.log(`Shard ${client.shard.ids[0]} ready with ${client.guilds.cache.size} guilds`); }); // Cross-shard data async function getTotalGuilds() { const results = await client.shard.fetchClientValues('guilds.cache.size'); return results.reduce((acc, count) => acc + count, 0); } // Broadcast to all shards async function broadcastMessage(channelId, message) { await client.shard.broadcastEval( (c, { channelId, message }) => { const channel = c.channels.cache.get(channelId); if (channel) channel.send(message); }, { context: { channelId, message } } ); }
# Pycord - AutoShardedBot import discord from discord.ext import commands # Automatically handles sharding bot = commands.AutoShardedBot( command_prefix="!", intents=discord.Intents.default(), shard_count=None # Auto-determine ) @bot.event async def on_ready(): print(f"Logged in on {len(bot.shards)} shards") for shard_id, shard in bot.shards.items(): print(f"Shard {shard_id}: {shard.latency * 1000:.2f}ms") @bot.event async def on_shard_ready(shard_id): print(f"Shard {shard_id} is ready") # Get guilds per shard for shard_id, guilds in bot.guilds_by_shard().items(): print(f"Shard {shard_id}: {len(guilds)} guilds")
Scaling_guide
- 1-2500 guilds: No sharding required
- 2500+ guilds: Sharding required by Discord
- Recommended: ~1000 guilds per shard
- Memory: Each shard runs in separate process
Sharp Edges
Interaction Timeout (3 Second Rule)
Severity: CRITICAL
Situation: Handling slash commands, buttons, select menus, or modals
Symptoms: User sees "This interaction failed" or "The application did not respond." Command works locally but fails in production. Slow operations never complete.
Why this breaks: Discord requires ALL interactions to be acknowledged within 3 seconds:
- Slash commands
- Button clicks
- Select menu selections
- Context menu commands
If you do ANY slow operation (database, API, file I/O) before responding, you'll miss the window. Discord shows an error even if your bot processes the request correctly afterward.
After acknowledgment, you have 15 minutes for follow-up responses.
Recommended fix:
Acknowledge immediately, process later
// Discord.js - Defer for slow operations module.exports = { async execute(interaction) { // DEFER IMMEDIATELY - before any slow operation await interaction.deferReply(); // For ephemeral: await interaction.deferReply({ ephemeral: true }); // Now you have 15 minutes const result = await slowDatabaseQuery(); const aiResponse = await callLLM(result); // Edit the deferred reply await interaction.editReply(`Result: ${aiResponse}`); } };
# Pycord @bot.slash_command() async def slow_command(ctx): await ctx.defer() # Acknowledge immediately # await ctx.defer(ephemeral=True) # For private response result = await slow_operation() await ctx.followup.send(f"Result: {result}")
For components (buttons, menus)
// If you're updating the message await interaction.deferUpdate(); // If you're sending a new response await interaction.deferReply({ ephemeral: true });
Missing Privileged Intent Configuration
Severity: CRITICAL
Situation: Bot needs member data, presences, or message content
Symptoms: Members intent: member lists empty, on_member_join doesn't fire Presences intent: statuses always unknown/offline Message content intent: message.content is empty string
Why this breaks: Discord has 3 privileged intents that require manual enablement:
- GUILD_MEMBERS - Member join/leave, member lists
- GUILD_PRESENCES - Online status, activities
- MESSAGE_CONTENT - Read message text (deprecated for commands)
These must be:
- Enabled in Discord Developer Portal > Bot > Privileged Gateway Intents
- Requested in your bot code
At 100+ servers, you need Discord verification to keep using them.
Recommended fix:
Step 1: Enable in Developer Portal
1. Go to https://discord.com/developers/applications 2. Select your application 3. Go to Bot section 4. Scroll to Privileged Gateway Intents 5. Toggle ON the intents you need
Step 2: Request in code
// Discord.js const { Client, GatewayIntentBits } = require('discord.js'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, // PRIVILEGED // GatewayIntentBits.GuildPresences, // PRIVILEGED // GatewayIntentBits.MessageContent, // PRIVILEGED - avoid! ] });
# Pycord intents = discord.Intents.default() intents.members = True # PRIVILEGED # intents.presences = True # PRIVILEGED # intents.message_content = True # PRIVILEGED - avoid! bot = commands.Bot(intents=intents)
Avoid Message Content Intent if possible
Use slash commands, buttons, and modals instead of message parsing. These don't require the Message Content intent.
Command Registration Rate Limited
Severity: HIGH
Situation: Registering slash commands
Symptoms: Commands not appearing. 429 errors when deploying. "You are being rate limited" messages. Commands appear for some guilds but not others.
Why this breaks: Command registration is rate limited:
- Global commands: 200 creates/day, updates take up to 1 hour to propagate
- Guild commands: 200 creates/day per guild, instant update
Common mistakes:
- Registering commands on every bot startup
- Registering in every guild separately
- Making changes in a loop without delays
Recommended fix:
Use a separate deploy script (not on startup)
// deploy-commands.js - Run manually, not on bot start const { REST, Routes } = require('discord.js'); const rest = new REST().setToken(process.env.DISCORD_TOKEN); async function deploy() { // For development: Guild commands (instant) if (process.env.GUILD_ID) { await rest.put( Routes.applicationGuildCommands( process.env.CLIENT_ID, process.env.GUILD_ID ), { body: commands } ); console.log('Guild commands deployed instantly'); } // For production: Global commands (up to 1 hour) else { await rest.put( Routes.applicationCommands(process.env.CLIENT_ID), { body: commands } ); console.log('Global commands deployed (may take up to 1 hour)'); } } deploy();
# Pycord - Don't sync on every startup @bot.event async def on_ready(): # DON'T DO THIS: # await bot.sync_commands() print(f"Ready! Commands should already be registered.") # Instead, sync manually or use a flag if __name__ == "__main__": if "--sync" in sys.argv: # Only sync when explicitly requested bot.sync_commands_on_start = True bot.run(token)
Testing workflow
- Use guild commands during development (instant updates)
- Only deploy global commands when ready for production
- Run deploy script manually, not on every restart
Bot Token Exposed
Severity: CRITICAL
Situation: Storing or sharing bot token
Symptoms: Unauthorized actions from your bot. Bot joins random servers. Bot sends spam or malicious content. "Invalid token" after Discord invalidates it.
Why this breaks: Your bot token provides FULL control over your bot. Attackers can:
- Send messages as your bot
- Join servers, create invites
- Access all data your bot can access
- Potentially take over servers where bot has admin
Discord actively scans GitHub for exposed tokens and invalidates them. Common exposure points:
- Committed to Git
- Shared in Discord itself
- In client-side code
- In public screenshots
Recommended fix:
Never hardcode tokens
// BAD - never do this const token = 'MTIzNDU2Nzg5MDEyMzQ1Njc4.ABCDEF.xyz...'; // GOOD - environment variables require('dotenv').config(); client.login(process.env.DISCORD_TOKEN);
Use .gitignore
# .gitignore .env .env.local config.json
If token is exposed
- Go to Developer Portal immediately
- Regenerate the token
- Update all deployments
- Review bot activity for unauthorized actions
- Check git history and force push to remove if needed
Use environment variables properly
# .env (never commit) DISCORD_TOKEN=your_token_here CLIENT_ID=your_client_id
// Load with dotenv require('dotenv').config(); const token = process.env.DISCORD_TOKEN;
Bot Missing applications.commands Scope
Severity: HIGH
Situation: Slash commands not appearing for users
Symptoms: Bot is in server but slash commands don't show up. Typing / shows no commands from your bot. Commands worked in development server but not others.
Why this breaks: Discord has two important OAuth scopes:
- Traditional bot permissions (messages, reactions, etc.)bot
- Slash command permissionsapplications.commands
Many bots were invited with only the
bot scope before slash commands
existed. They need to be re-invited with both scopes.
Recommended fix:
Generate correct invite URL
https://discord.com/api/oauth2/authorize ?client_id=YOUR_CLIENT_ID &permissions=0 &scope=bot%20applications.commands
In Discord Developer Portal
- Go to OAuth2 > URL Generator
- Select BOTH:
botapplications.commands
- Select required bot permissions
- Use generated URL
Re-invite without kicking
Users can use the new invite URL even if bot is already in server. This adds the new scope without removing the bot.
// Generate invite URL in code const inviteUrl = client.generateInvite({ scopes: ['bot', 'applications.commands'], permissions: [ 'SendMessages', 'EmbedLinks', // Add other needed permissions ] });
Global Commands Not Appearing Immediately
Severity: MEDIUM
Situation: Deploying global slash commands
Symptoms: Commands don't appear after deployment. Guild commands work but global commands don't. Commands appear after an hour.
Why this breaks: Global commands can take up to 1 hour to propagate to all Discord servers. This is by design for Discord's caching and CDN.
Guild commands are instant but only work in that specific guild.
Recommended fix:
Development: Use guild commands
// Instant updates for testing await rest.put( Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: commands } );
Production: Deploy global commands during off-peak
// Takes up to 1 hour to propagate await rest.put( Routes.applicationCommands(CLIENT_ID), { body: commands } );
Workflow
- Develop and test with guild commands (instant)
- When ready, deploy global commands
- Wait up to 1 hour for propagation
- Don't deploy global commands frequently
Frequent Gateway Disconnections
Severity: MEDIUM
Situation: Bot randomly goes offline or misses events
Symptoms: Bot shows as offline intermittently. Events are missed (member joins, messages). Reconnection messages in logs.
Why this breaks: Discord gateway requires regular heartbeats. Issues:
- Blocking operations prevent heartbeat
- Network instability
- Memory pressure causing GC pauses
- Too many guilds without sharding (2500+ requires sharding)
Recommended fix:
Never block the event loop
// BAD - blocks event loop const data = fs.readFileSync('file.json'); // GOOD - async const data = await fs.promises.readFile('file.json');
Handle reconnections gracefully
client.on('shardResume', (id, replayedEvents) => { console.log(`Shard ${id} resumed, replayed ${replayedEvents} events`); }); client.on('shardDisconnect', (event, id) => { console.log(`Shard ${id} disconnected`); }); client.on('shardReconnecting', (id) => { console.log(`Shard ${id} reconnecting...`); });
Implement sharding at scale
// Required at 2500+ guilds const manager = new ShardingManager('./bot.js', { token: process.env.DISCORD_TOKEN, totalShards: 'auto' }); manager.spawn();
Modal Must Be First Response
Severity: MEDIUM
Situation: Showing a modal from a slash command or button
Symptoms: "Interaction has already been acknowledged" error. Modal doesn't appear. Works sometimes but not others.
Why this breaks: Modals have a special requirement: showing a modal MUST be the first response to an interaction. You cannot:
- defer() then showModal()
- reply() then showModal()
- Think for more than 3 seconds then showModal()
Recommended fix:
Show modal immediately
// CORRECT - modal is first response async execute(interaction) { const modal = new ModalBuilder() .setCustomId('my-modal') .setTitle('Input Form'); // Show immediately - no defer, no reply first await interaction.showModal(modal); }
// WRONG - deferred first async execute(interaction) { await interaction.deferReply(); // CAN'T DO THIS await interaction.showModal(modal); // Will fail }
If you need to check something first
async execute(interaction) { // Quick sync check is OK (under 3 seconds) if (!hasPermission(interaction.user.id)) { return interaction.reply({ content: 'No permission', ephemeral: true }); } // Show modal (still first interaction response for this path) await interaction.showModal(modal); }
Validation Checks
Hardcoded Discord Token
Severity: ERROR
Discord tokens must never be hardcoded
Message: Hardcoded Discord token detected. Use environment variables.
Token Variable Assignment
Severity: ERROR
Tokens should come from environment, not strings
Message: Token assigned from string literal. Use environment variable.
Token in Client-Side Code
Severity: ERROR
Never expose Discord tokens to browsers
Message: Discord credentials exposed client-side. Only use server-side.
Slow Operation Without Defer
Severity: WARNING
Slow operations should be deferred to avoid timeout
Message: Slow operation without defer. Interaction may timeout.
Interaction Without Error Handling
Severity: WARNING
Interactions should have try/catch for graceful errors
Message: Interaction without error handling. Add try/catch.
Using Message Content Intent
Severity: WARNING
Message Content is privileged, prefer slash commands
Message: Using Message Content intent. Consider slash commands instead.
Requesting All Intents
Severity: WARNING
Only request intents you actually need
Message: Requesting all intents. Only enable what you need.
Syncing Commands on Ready Event
Severity: WARNING
Don't sync commands on every bot startup
Message: Syncing commands on startup. Use separate deploy script.
Registering Commands in Loop
Severity: WARNING
Use bulk registration, not individual calls
Message: Registering commands in loop. Use bulk registration.
No Rate Limit Handling
Severity: INFO
Consider handling rate limits for bulk operations
Message: Bulk operation without rate limit handling.
Collaboration
Delegation Triggers
- user needs AI-powered Discord bot -> llm-architect (Integrate LLM for conversational Discord bot)
- user needs Slack integration too -> slack-bot-builder (Cross-platform bot architecture)
- user needs voice features -> voice-agents (Discord voice channel integration)
- user needs database for bot data -> postgres-wizard (Store user data, server configs, moderation logs)
- user needs workflow automation -> workflow-automation (Discord events trigger workflows)
- user needs high availability -> devops (Sharding, scaling, monitoring for large bots)
- user needs payment integration -> stripe-specialist (Premium bot features, subscription management)
When to Use
Use this skill when the request clearly matches the capabilities and patterns described above.
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.