Discord Bots skill for Codex

Slash commands that always answer in time. Codex builds Discord bots around application commands and components instead of message parsing, defers any interaction that may take longer than three seconds, and registers commands from a separate deploy script. It requests only the gateway intents the features use, verifies HTTP interactions with Ed25519, and develops in a private test guild before anything reaches real servers.

  • Integrations
  • $discord-bots
  • 187 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- discord-bots

Installs to ~/.agents/skills/discord-bots/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $discord-bots, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Discord bots with discord.js v14 or discord.py 2.x around application commands - slash command registration, interaction replies and deferrals, buttons, select menus, modals, autocomplete, gateway intents, permissions and HTTP interaction endpoints with Ed25519 verification. Use when creating a Discord bot, adding slash commands, fixing "The application did not respond" or Unknown interaction errors, choosing gateway intents, or deploying a bot to production.

Use it when

  • Starting a Discord bot or adding slash commands, buttons or modals
  • Interactions fail with Unknown interaction or no response errors
  • Choosing gateway intents or preparing for bot verification
  • Running Discord commands from a serverless HTTP endpoint

Use something else when

How to check the result

Every command, button and modal works in a private test guild, a handler with an artificial four-second delay still succeeds through deferReply, and the invite URL lists only needed permissions.

The mistake it prevents

Doing slow work before replying, so the three-second interaction window closes and Discord shows The application did not respond.

~/.agents/skills/discord-bots/SKILL.md

--- name: discord-bots description: Builds Discord bots with discord.js v14 or discord.py 2.x around application commands - slash command registration, interaction replies and… license: MIT ---

Done well means: the bot uses slash commands and components rather than reading message content, every interaction is answered or deferred within 3 seconds, it requests only the intents and permissions it uses, commands are registered by a separate deploy script, and the token never appears in code, logs or git history.

Before writing code

  1. Read AGENTS.md and the package manifest. Check the library and version: discord.js 14.x (Node 18+) or discord.py 2.x (app_commands). Older code using message.content prefixes or discord.js v12/v13 APIs needs migration, not extension.
  2. Secrets: DISCORD_TOKEN, DISCORD_CLIENT_ID (application ID), DISCORD_GUILD_ID for a dev server. If a token was ever committed, tell the user to reset it in the Developer Portal.
  3. Decide the runtime:
SituationChoose
Bot reacts to events (joins, messages, voice) or keeps stateGateway bot (long-running process)
Only slash commands and components, serverless hostingHTTP interactions endpoint (no gateway)
  1. Sending messages to real servers, mass DMs, or role and moderation actions (kick, ban, delete messages) need explicit user confirmation. Develop in a private test guild.

Installing packages and connecting to Discord need network; in a Codex sandbox, ask for approval, or unit-test handlers with stubbed interaction objects.

Intents and permissions

IntentPrivilegedNeeded for
GuildsNoAlmost everything; slash commands
GuildMessagesNoMessage events (metadata only without MessageContent)
MessageContentYesReading message text outside mentions and DMs
GuildMembersYesMember join/leave, full member lists
GuildPresencesYesOnline status, activities

Privileged intents must be toggled in the Developer Portal; once the bot is in 100 or more servers it needs verification and approval for them. Design around slash commands so you do not need MessageContent. Generate the invite URL with scopes bot applications.commands and only the permissions the commands use.

Project layout (discord.js)

src/
  commands/ping.ts        # { data: SlashCommandBuilder, execute(interaction) }
  index.ts                # client, event routing
  deploy-commands.ts      # registers commands via REST, run on demand
// src/commands/remind.ts
import { SlashCommandBuilder, MessageFlags, type ChatInputCommandInteraction } from "discord.js";

export const data = new SlashCommandBuilder()
  .setName("remind")
  .setDescription("Set a reminder")
  .addStringOption((o) => o.setName("what").setDescription("Reminder text").setRequired(true).setMaxLength(200))
  .addIntegerOption((o) => o.setName("minutes").setDescription("Delay").setMinValue(1).setMaxValue(10080).setRequired(true));

export async function execute(interaction: ChatInputCommandInteraction) {
  await interaction.deferReply({ flags: MessageFlags.Ephemeral }); // buys 15 minutes
  const what = interaction.options.getString("what", true);
  const minutes = interaction.options.getInteger("minutes", true);
  await saveReminder(interaction.user.id, interaction.channelId, what, minutes);
  await interaction.editReply(`Reminder set for ${minutes} min from now.`);
}
// src/index.ts
import { Client, Events, GatewayIntentBits } from "discord.js";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  const command = commands.get(interaction.commandName);
  try {
    await command?.execute(interaction);
  } catch (err) {
    console.error(err);
    const msg = { content: "Something went wrong.", flags: MessageFlags.Ephemeral } as const;
    interaction.deferred || interaction.replied ? await interaction.followUp(msg) : await interaction.reply(msg);
  }
});
client.login(process.env.DISCORD_TOKEN);
// src/deploy-commands.ts: guild commands for dev, global for production
import { REST, Routes } from "discord.js";
const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
const body = allCommands.map((c) => c.data.toJSON());
const route = process.env.DISCORD_GUILD_ID
  ? Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID!, process.env.DISCORD_GUILD_ID)
  : Routes.applicationCommands(process.env.DISCORD_CLIENT_ID!);
await rest.put(route, { body });

Never register commands on every startup; it wastes rate limit and can wipe commands when the list is incomplete. PUT replaces the whole set.

discord.py equivalent

import discord
from discord import app_commands

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
DEV_GUILD = discord.Object(id=int(os.environ["DISCORD_GUILD_ID"]))

@tree.command(name="remind", description="Set a reminder", guild=DEV_GUILD)
@app_commands.describe(what="Reminder text", minutes="Delay in minutes")
async def remind(interaction: discord.Interaction, what: str, minutes: app_commands.Range[int, 1, 10080]):
    await interaction.response.defer(ephemeral=True)
    await save_reminder(interaction.user.id, what, minutes)
    await interaction.followup.send(f"Reminder set for {minutes} min.")

@client.event
async def setup_hook():
    await tree.sync(guild=DEV_GUILD)   # move to a manual command or script for production

Response rules

SituationDo
Work under 3 sreply()
Work may exceed 3 sdeferReply() immediately, then editReply() within 15 min
Button or select clickupdate() to edit the original, or deferUpdate() then edit
Need a formshowModal() as the first response (cannot follow a defer)
Autocompleterespond() with up to 25 choices within 3 s; no deferral possible

Component customId is at most 100 characters; encode an action and an ID (approve:1234), keep larger state in your database. A message holds up to 5 action rows of up to 5 buttons. Message content is capped at 2,000 characters; split or attach a file beyond that.

HTTP interactions endpoint

Discord signs each request with Ed25519: headers X-Signature-Ed25519 and X-Signature-Timestamp, message = timestamp + raw body, verified with the application’s public key. Reply to type 1 (PING) with { "type": 1 }. Use a maintained verifier (for example the discord-interactions package’s verifyKey) rather than hand-rolling crypto, and verify before parsing JSON. Discord sends invalid signatures on purpose when you save the URL to check that you reject them.

Gotchas

  • “The application did not respond”: no reply or defer within 3 s, or the handler threw first.
  • “Unknown interaction” (10062): replying after 3 s without deferring, or replying twice.
  • Ephemeral: use flags: MessageFlags.Ephemeral; the ephemeral: true option is deprecated in current discord.js 14 releases.
  • Global commands can take time to show in clients; develop with guild commands.
  • Rate limits are handled by the library’s REST queue; do not wrap calls in your own tight retry loops. Bulk DMs and mass mentions get bots flagged.
  • Sharding is required from 2,500 guilds (ShardingManager in discord.js, AutoShardedClient in discord.py).
  • Permission checks: set setDefaultMemberPermissions on admin commands and still check interaction.memberPermissions server-side.

Verification

  • Run deploy-commands against the test guild, then invoke each command, button, select and modal; check both success and error paths.
  • Add an artificial 4 s delay to one handler in a branch and confirm deferral keeps it working.
  • For HTTP endpoints, saving the Interactions Endpoint URL in the portal succeeds (Discord’s validation pings pass).
  • Confirm the invite URL requests only the listed permissions.

Report template

**Library:** discord.js 14.x, gateway bot, intents: Guilds
**Commands:** /remind (string, integer), /reminders (list, ephemeral), registered via deploy-commands.ts
**Components:** cancel button customId cancel:<reminderId>
**Permissions:** Send Messages, Embed Links (invite URL in README)
**Tested in:** private test guild; production command registration awaiting approval

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Discord Bots"
  short_description: "Slash commands that always answer in time"
  default_prompt: "$discord-bots Create a Discord bot with a /remind slash command using discord.js"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem