with one click
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.
// 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.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | discord-bot-architect |
| description | 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. |
| risk | unknown |
| source | vibeship-spawner-skills (Apache 2.0) |
| date_added | "2026-02-27T00:00:00.000Z" |
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.
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);
}
})();
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
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))
discord-bot/ ├── main.py # Main bot file ├── cogs/ # Command groups │ └── general.py ├── .env └── requirements.txt
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())
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")
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)
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!"))
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")
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:
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:
// 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}")
// If you're updating the message
await interaction.deferUpdate();
// If you're sending a new response
await interaction.deferReply({ ephemeral: true });
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:
These must be:
At 100+ servers, you need Discord verification to keep using them.
Recommended fix:
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
// 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)
Use slash commands, buttons, and modals instead of message parsing. These don't require the Message Content intent.
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:
Common mistakes:
Recommended fix:
// 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)
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:
Discord actively scans GitHub for exposed tokens and invalidates them. Common exposure points:
Recommended fix:
// BAD - never do this
const token = 'MTIzNDU2Nzg5MDEyMzQ1Njc4.ABCDEF.xyz...';
// GOOD - environment variables
require('dotenv').config();
client.login(process.env.DISCORD_TOKEN);
# .gitignore
.env
.env.local
config.json
# .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;
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:
bot - Traditional bot permissions (messages, reactions, etc.)applications.commands - Slash command permissionsMany bots were invited with only the bot scope before slash commands
existed. They need to be re-invited with both scopes.
Recommended fix:
https://discord.com/api/oauth2/authorize
?client_id=YOUR_CLIENT_ID
&permissions=0
&scope=bot%20applications.commands
botapplications.commandsUsers 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
]
});
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:
// Instant updates for testing
await rest.put(
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
{ body: commands }
);
// Takes up to 1 hour to propagate
await rest.put(
Routes.applicationCommands(CLIENT_ID),
{ body: commands }
);
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:
Recommended fix:
// BAD - blocks event loop
const data = fs.readFileSync('file.json');
// GOOD - async
const data = await fs.promises.readFile('file.json');
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...`);
});
// Required at 2500+ guilds
const manager = new ShardingManager('./bot.js', {
token: process.env.DISCORD_TOKEN,
totalShards: 'auto'
});
manager.spawn();
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:
Recommended fix:
// 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
}
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);
}
Severity: ERROR
Discord tokens must never be hardcoded
Message: Hardcoded Discord token detected. Use environment variables.
Severity: ERROR
Tokens should come from environment, not strings
Message: Token assigned from string literal. Use environment variable.
Severity: ERROR
Never expose Discord tokens to browsers
Message: Discord credentials exposed client-side. Only use server-side.
Severity: WARNING
Slow operations should be deferred to avoid timeout
Message: Slow operation without defer. Interaction may timeout.
Severity: WARNING
Interactions should have try/catch for graceful errors
Message: Interaction without error handling. Add try/catch.
Severity: WARNING
Message Content is privileged, prefer slash commands
Message: Using Message Content intent. Consider slash commands instead.
Severity: WARNING
Only request intents you actually need
Message: Requesting all intents. Only enable what you need.
Severity: WARNING
Don't sync commands on every bot startup
Message: Syncing commands on startup. Use separate deploy script.
Severity: WARNING
Use bulk registration, not individual calls
Message: Registering commands in loop. Use bulk registration.
Severity: INFO
Consider handling rate limits for bulk operations
Message: Bulk operation without rate limit handling.
Use this skill when the request clearly matches the capabilities and patterns described above.