Dashboard

How to Build a Discord Bot With AI

An AI agent writes the interesting parts of a Discord bot correctly on the first try. What it gets wrong is Discord's platform rules.

Steve Jefferson
Steve Jefferson
Developer Advocate
9 September 20261 min read

How to Build a Discord Bot With AI

You can get a working Discord bot from an AI coding agent in an afternoon, and the model will write the interesting parts correctly on the first try. What it usually gets wrong is Discord's platform rules: the three-second deadline on every interaction, which gateway intents you are allowed to request, and where commands need to be registered so they appear at all. Those four things account for most of the "the bot is online but nothing happens" reports.

This walks through the build in the order that avoids them.

Decide what the bot does before you open the editor

Discord bots fall into three shapes, and the shape determines almost every technical decision afterwards.

  • Command bot. Responds to slash commands. /weather london, /roll 2d6, /ticket open. Simplest by a wide margin, and the right default.

  • Listener bot. Reads messages and reacts to their content. Auto-moderation, keyword alerts, an AI assistant that answers questions in a channel. Requires the privileged Message Content intent.

  • Scheduled bot. Posts on a timer or in response to an external webhook. Standup reminders, deploy notifications, RSS relays. Needs somewhere to run continuously.

If you can express what you want as a command bot, do. Because bots receive slash commands as structured events rather than by reading everything anyone says, you avoid the privileged-intent review process entirely, and users get autocomplete and argument validation for free.

A Discord bot is a persistent backend service rather than a page-and-form application, which makes it a different shape of project from most of what people build with AI. If you are new to the general workflow, start with how to build an app with AI. The closest sibling to this build is making a Slack bot with AI, which shares the command-handling structure and almost none of the platform rules.

What to give the agent before it writes anything

Agents write better Discord code when they are told which library and which version. The API has changed shape several times, and models will happily produce discord.js v12 code that no longer runs.

A prompt preamble that works:

text
Build a Discord bot using discord.js v14 and Node 20, in TypeScript.
Use slash commands registered per-guild during development.
Every command handler must call deferReply() first if it does any I/O.
Do not use the MessageContent intent; the bot only responds to slash commands.
Put the bot token in an environment variable, never inline.

Each of those lines is preventing a specific mistake. The version pin stops it reaching for an old API. The deferReply() rule prevents the timeout failure described below. Saying explicitly which intents to avoid stops it requesting privileged ones out of habit, which would then need review. Our guide on writing prompts that stop an agent inventing APIs covers the general version of this.

Step 1: create the application and get a token

This part is manual and takes about four minutes.

  1. Create an application in the Discord Developer Portal and add a bot user to it.

  2. Copy the bot token once. It is shown a single time; regenerate it if you lose it.

  3. Under OAuth2, generate an invite URL with the bot and applications.commands scopes and the permissions your bot actually needs. Grant the narrowest set that works.

  4. Note your application ID and, for development, the ID of a test server.

Keep the token out of the repository from the first commit. Agents are good at reading a .env file and less good at remembering not to paste its contents into a code comment. The handling pattern is in environment variables and secrets in an AI-built app.

Step 2: register commands where you will actually see them

This is the single most common reason a bot appears to do nothing.

Guild commands register instantly against one server. Global commands propagate across Discord and can take up to an hour to appear. Register per-guild during development and switch to global only when you ship.

ts
import { REST, Routes, SlashCommandBuilder } from 'discord.js';

const commands = [
  new SlashCommandBuilder()
    .setName('summarise')
    .setDescription('Summarise the last N messages in this channel')
    .addIntegerOption(o =>
      o.setName('count').setDescription('How many messages').setMinValue(5).setMaxValue(100))
].map(c => c.toJSON());

const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!);

await rest.put(
  Routes.applicationGuildCommands(process.env.APP_ID!, process.env.DEV_GUILD_ID!),
  { body: commands }
);

Registration is a separate script you run when commands change, not something that belongs in your bot's startup path. Agents frequently put it in startup, which works but re-registers on every restart and eventually trips rate limits.

Step 3: handle the three-second deadline

Discord expects an acknowledgement within three seconds of an interaction. Anything slower produces "The application did not respond" for the user, and the interaction token is then dead.

Almost anything interesting takes longer than three seconds, so the pattern is: defer immediately, do the work, edit the reply.

ts
client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== 'summarise') return;

  await interaction.deferReply();                 // buys you ~15 minutes

  try {
    const count = interaction.options.getInteger('count') ?? 20;
    const messages = await interaction.channel!.messages.fetch({ limit: count });
    const text = [...messages.values()].reverse()
      .map(m => `${m.author.username}: ${m.content}`).join('\n');

    const summary = await summarise(text);        // your model call
    await interaction.editReply(summary.slice(0, 2000));
  } catch (err) {
    await interaction.editReply('Could not summarise that. Try a smaller count.');
  }
});

Three details in that snippet that agents routinely miss. deferReply() comes before any I/O, not after the first await. The reply is truncated to 2,000 characters, because that is Discord's message limit and exceeding it throws. The catch block edits the deferred reply rather than sending a new one, because a deferred interaction that never gets edited leaves a permanent "thinking" state.

Step 4: request only the intents you need

Gateway intents tell Discord which categories of event to send you. Three are privileged and off by default: Guild Presences, Guild Members, and Message Content.

Per Discord's documentation on privileged intents, bots in fewer than 100 servers can usually enable these in the Developer Portal directly, while bots in 100 or more servers must apply and be approved for the privileged intents they use. Planning to grow past 100 servers with a Message Content dependency you cannot justify is a problem you get to discover later, at the worst time.

ts
import { Client, GatewayIntentBits } from 'discord.js';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

Guilds alone is enough for a slash-command bot. The discord.js guide on intents has the full matrix of which events each intent unlocks.

Step 5: the AI part, if the bot has one

If the bot calls a model, two constraints are specific to Discord and worth handing to your agent explicitly.

The 2,000 character message limit means long model output has to be split, truncated, or sent as a file attachment. Splitting mid-code-block renders badly, so split on paragraph boundaries and re-open fences.

Channel context is not a conversation. A Discord channel is many people talking at once, often about several things. Feeding the last 50 messages to a model as chat history produces confused output. Either scope to a thread, scope to one user's messages, or make the command take explicit input. Keeping a bot's answers on topic is a general problem, covered in how to keep an AI chatbot on topic.

Step 6: deploy it somewhere that stays awake

A gateway bot holds a persistent WebSocket connection, which rules out most serverless platforms. Options that work:

  • A small always-on container or VM. Cheapest and simplest for one bot.

  • A platform with long-running process support, rather than a request-scoped function runtime.

  • HTTP interactions instead of the gateway. Discord can POST interactions to an endpoint you host, which does work on serverless, but you lose every event that is not an interaction, and you must verify request signatures.

Set a restart policy. Gateway connections drop, and discord.js reconnects on its own, but the process still dies occasionally. The general deployment checklist is in how to deploy an app built with AI.

Common failures, in the order you will hit them

Symptom

Cause

Commands do not appear

Registered globally and still propagating, or registered to the wrong guild

"The application did not respond"

No deferReply() before slow work

Bot online, ignores messages

Message Content intent not enabled, or not requested in code

Reply stuck on "thinking"

Deferred interaction never edited, usually an unhandled error

Random 429 errors

Re-registering commands on every restart

Works locally, dies in production

Serverless runtime killing the WebSocket

FAQ

Can AI write a whole Discord bot without me understanding the code?

For a small command bot, mostly yes. The parts that need your judgement are permission scopes on the invite URL and which intents you request, because both are security decisions that are awkward to reverse once the bot is in servers.

Which library should I ask for?

discord.js for TypeScript or JavaScript, discord.py for Python. Pin the major version in your prompt. The discord.js docs are the reference to point an agent at if it starts producing outdated patterns.

Why do my slash commands take an hour to show up?

You registered them globally. Global commands propagate slowly. Register per-guild during development for instant updates and switch to global at release.

Do I need the Message Content intent?

Only if the bot reads messages it was not explicitly invoked on. A slash-command bot does not need it, and avoiding it means you never go through privileged intent review.

How do I handle model responses longer than 2,000 characters?

Split on paragraph boundaries and send follow-ups, or attach the full text as a file and put a short summary in the message. Never let the raw response reach editReply() unbounded, because it throws rather than truncating.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.