October 13, 2026 · 11 min read · by the Discordfinder team

How to make a Discord bot in 2026: portal, token, intents and your first slash command

Create the app, protect the token, pick intents under the June 2026 rules, invite it, and run a slash-command bot in Python or JavaScript, then host it and get it found.

How to make a Discord bot in 2026 comes down to five steps: create an application in the Discord Developer Portal, copy its bot token into an environment variable, choose the smallest set of intents you need, install the bot with the bot and applications.commands scopes, and run a short program that registers a slash command and answers it. Below are those steps with exact menu paths, a working bot in both Python (discord.py 2.7.1) and JavaScript (discord.js 14.27.0), the intent rules that changed on 10 June 2026, and what to do once the bot works and you want people to find it.

What you need before you start

Python JavaScript
Library discord.py 2.7.1 (March 2026) discord.js 14.27.0 (July 2026)
Runtime the library requires Python 3.8 or higher Node.js 18 or newer
What to actually install A current Python 3 release A current Node.js LTS release
Install command pip install -U discord.py python-dotenv npm install discord.js

The stated minimums are old; Python 3.8 and Node.js 18 are both past end of life, so use a supported release. You also need a Discord account and a test server you own. Build in a server nobody else uses, as Discord's own tutorial recommends, so a broken command never lands in front of real members.

Step 1: Create the application

  1. Open the Discord Developer Portal and click New Application.
  2. Give it a name and press Create. You land on General Information, which shows the Application ID. Copy it; the JavaScript example needs it, and so does every bot listing form.
  3. Set an icon and a description here now. They appear on the bot's profile and on the install screen.

New applications get a bot user automatically. Open the Bot page in the left sidebar. Two settings matter before anything else:

  • Public Bot. If it is unchecked, only you can add the bot to servers. Leave it off while you build, and switch it on when you want other people to install it.
  • Privileged Gateway Intents. Leave all three off for now. Step 3 explains when you need them.

Step 2: Get the token and keep it out of Git

On the Bot page, under Token, click Reset Token. Discord shows the new token once; it will not show it again unless you reset it, which invalidates the old one. The token is the bot's password. Anyone holding it can log in as your bot and do everything its permissions allow in every server it is in.

Put it in a .env file in your project folder and nowhere else:

DISCORD_TOKEN=paste-the-token-here
GUILD_ID=your-test-server-id
CLIENT_ID=your-application-id

Then make sure Git never sees that file:

echo ".env" >> .gitignore

To get GUILD_ID, turn on User Settings → Advanced → Developer Mode, then right-click your test server's icon and choose Copy Server ID.

Three rules cover almost every token leak:

  • Never paste the token into code, screenshots, support channels or an AI chat. Read it from the environment.
  • On a host, use the platform's secrets or variables panel instead of uploading .env.
  • If it leaks, reset it immediately on the Bot page, then update the host. Discord has been a GitHub secret scanning partner since 2019, so a token pushed to a public repository can be revoked before you notice. Your bot simply stops logging in until you set the new one.

Step 3: Choose intents, and know the June 2026 rules

Intents decide which events Discord sends your bot over the Gateway. Most are standard and need no approval. Three are privileged and gated:

Portal toggle What it unlocks Do you need it for a slash-command bot?
Presence Intent Online status, activities and platform of members No
Server Members Intent Member join, leave and update events, and listing every member of a server Only for welcome messages, member logs or role sync
Message Content Intent The content, embeds, attachments, components and poll fields of messages No. Slash commands do not need it

Without Message Content, your bot still sees the content of messages it sends, messages in DMs with it, messages that mention it, and the target of a message context-menu command. Discord's own privileged intent guide says text commands are "the most common reason developers request" Message Content, and recommends slash commands instead.

What changed on 10 June 2026. The review threshold for privileged intents used to be 100 servers. It is now based on users: once more than 10,000 unique users can see your app across all the servers it is in, and it has a privileged intent enabled, you get a notice and 90 days to apply for access. Below 10,000 users you can toggle the intents yourself on the Bot page. Approved apps now reapply every year, and an app keeps its intents and can keep joining servers while a review is pending. Discord's example is that a bot in 50 large servers can hit the threshold while one in 200 small servers might not.

What did not change. App verification is still required for apps in 100 or more servers; the Gateway documentation still says so in as many words. The App Verification page in the Developer Portal lists the current requirements, and the team owner has to verify their identity through Stripe.

Enabling an intent takes two switches: the toggle on the Bot page and the intent in your code. If your code requests a privileged intent the portal has not enabled, Discord closes the connection with close code 4014. In discord.py that surfaces as PrivilegedIntentsRequired; in discord.js as "Used disallowed intents".

Step 4: Build the invite link

Open the Installation page:

  1. Under Installation Contexts, keep Guild Install ticked. User Install lets people add the bot to their own account and use its commands anywhere, but a server-focused bot can leave it off.
  2. Under Install Link, choose Discord Provided Link.
  3. Under Default Install Settings → Guild Install, add the scopes bot and applications.commands. A Permissions list appears once bot is selected.
  4. Pick only the permissions the bot needs, then copy the link and open it in your browser to add the bot to your test server.

Adding a bot to a server requires the Manage Server permission there. If you prefer to build the URL by hand, it looks like this, where permissions is the sum of the permission bits:

https://discord.com/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot+applications.commands&permissions=2048
Permission Value Needed when
View Channels 1024 The bot reads or posts in channels
Send Messages 2048 The bot posts outside command replies
Embed Links 16384 The bot posts embeds
Read Message History 65536 The bot reads earlier messages
Administrator 8 Never for a public bot

All four useful ones together come to 84992. Asking for Administrator is the quickest way to get a bot rejected by careful server owners, and every directory reviewer notices it.

Step 5: A minimal slash-command bot in Python

Create bot.py:

import os
import random

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()
GUILD = discord.Object(id=int(os.environ["GUILD_ID"]))


class Bot(discord.Client):
    def __init__(self):
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        # Register commands in the test server only: guild commands update instantly
        self.tree.copy_global_to(guild=GUILD)
        await self.tree.sync(guild=GUILD)


bot = Bot()


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user} (ID: {bot.user.id})")


@bot.tree.command(description="Check that the bot is alive")
async def ping(interaction: discord.Interaction):
    await interaction.response.send_message(f"Pong! {round(bot.latency * 1000)} ms")


@bot.tree.command(description="Roll a die")
@app_commands.describe(sides="Number of sides, default 6")
async def roll(interaction: discord.Interaction, sides: app_commands.Range[int, 2, 1000] = 6):
    await interaction.response.send_message(f"You rolled {random.randint(1, sides)} on a d{sides}")


bot.run(os.environ["DISCORD_TOKEN"])

Run it with python bot.py. In your test server, type / and both commands appear under your bot's name.

What the code does: Intents.default() includes every standard intent and none of the privileged ones, which is exactly what a slash-command bot needs. The function's docstring or description becomes the command description, app_commands.Range makes Discord enforce 2 to 1000 in the client before your code runs, and setup_hook runs once before the bot connects, the place discord.py's own example uses for syncing.

Step 6: The same bot in JavaScript

Run npm init -y and npm install discord.js, then create index.js:

const { Client, Events, GatewayIntentBits, REST, Routes, SlashCommandBuilder } = require('discord.js');

const { DISCORD_TOKEN, CLIENT_ID, GUILD_ID } = process.env;

const commands = [
  new SlashCommandBuilder().setName('ping').setDescription('Check that the bot is alive'),
  new SlashCommandBuilder()
    .setName('roll')
    .setDescription('Roll a die')
    .addIntegerOption((o) =>
      o.setName('sides').setDescription('Number of sides, default 6').setMinValue(2).setMaxValue(1000)),
].map((c) => c.toJSON());

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once(Events.ClientReady, async (ready) => {
  // Register commands in the test server only: guild commands update instantly
  const rest = new REST().setToken(DISCORD_TOKEN);
  await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: commands });
  console.log(`Logged in as ${ready.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.reply(`Pong! ${client.ws.ping} ms`);
  } else if (interaction.commandName === 'roll') {
    const sides = interaction.options.getInteger('sides') ?? 6;
    const result = Math.floor(Math.random() * sides) + 1;
    await interaction.reply(`You rolled ${result} on a d${sides}`);
  }
});

client.login(DISCORD_TOKEN);

Run it with node --env-file=.env index.js. Node.js reads the .env file itself, so you do not need the dotenv package.

Two details that trip up people copying older tutorials: in discord.js 14 the ready event constant Events.ClientReady now maps to clientReady, and the old 'ready' event name now prints a deprecation warning and goes away in v15; and private replies use flags: MessageFlags.Ephemeral rather than the older ephemeral: true option. Command names must be 1 to 32 characters, lower case, with no spaces.

Guild commands, global commands and the 3-second rule

Both examples register guild commands, which Discord's documentation says update instantly and recommends for testing. When the bot is ready for other servers, register global commands instead: await self.tree.sync() with no guild in discord.py, or Routes.applicationCommands(CLIENT_ID) in discord.js. If you register the same command both ways, it shows up twice in your test server, so clear the guild copies when you switch.

Limits worth knowing:

  • 100 global slash commands per app, plus 15 user and 15 message context-menu commands.
  • 200 command creates per day, per server. Syncing on every start is fine while you test; for a public bot, move registration into a separate script you run when commands change, which is what the discord.js guide does.
  • Every interaction needs an initial response within 3 seconds, or Discord shows "The application did not respond". For slow work, defer first (interaction.response.defer() in discord.py, interaction.deferReply() in discord.js), then follow up. The interaction token stays valid for 15 minutes.

When it does not work

Symptom Usual cause Fix
Close code 4014, "disallowed intents" Code requests a privileged intent the portal has not enabled Toggle it on the Bot page, or remove it from your code
Login fails with an invalid token error Token was reset, revoked after a leak, or copied with a space Reset it on the Bot page and update .env
Commands do not appear Registered globally instead of to the guild, or bot added without applications.commands Sync to the guild; re-invite with both scopes
"The application did not respond" Handler took longer than 3 seconds or threw an error Defer early and check your console for the exception
Bot is online but cannot post Missing channel permission or a channel override Check the bot's role in Server Settings → Roles and the channel's permissions

Hosting: keeping it online

A Gateway bot like the two above holds a WebSocket connection open, so it needs a process that runs all the time. Your laptop works for development and stops the moment it sleeps. For real use you need a VPS, a container platform or a small always-on machine; we compare the options, prices and free tiers in how to host a Discord bot 24/7.

Two Discord rules shape hosting choices. First, Discord allows 1,000 Gateway IDENTIFY calls per 24 hours across all shards; a bot stuck in a crash-and-restart loop can burn through that, at which point Discord terminates its sessions and resets the token. Add a restart delay to whatever supervises the process. Second, a bot that only answers slash commands can skip the Gateway entirely and receive interactions over HTTP at an Interactions Endpoint URL, which suits serverless hosts; Discord publishes a Cloudflare Workers tutorial for that model.

Getting your bot discovered

A working bot with no users is still a private project. Before you share it:

  1. Switch Public Bot on and set the default install link from Step 4, with minimal permissions.
  2. Fill in the description, icon and a support server link on General Information, and keep a short command list ready.
  3. Plan for growth: verification at 100 servers, and privileged intent review once more than 10,000 users can see the app.

Discord's own App Directory only lists apps that have completed App Verification and opted into Discovery, so a new bot cannot start there. Directories fill that gap. On Discordfinder, listing a bot is free and asks for the Application ID, the invite URL, a short description of up to 240 characters, an example command and up to five tags; the bot then appears in the bot directory, and once you add it to the monitoring server it is pinged every 5 minutes, so a bot that stays offline for 24 hours is hidden rather than sending people to a dead invite. Our comparison of where to list a Discord bot covers the App Directory, Top.gg and Discordfinder side by side, and which bots a new server actually needs shows what server owners look for when they choose one.

Pick tags that describe what the bot does. A general helper fits utility bots; a developer tool belongs under programming or tech. For help with the code itself, the Python server and The Programmer's Hangout are both live on Discordfinder, and discord.py and discord.js each run their own support servers linked from their documentation.

Try it: get /ping answering in your test server, switch on Public Bot, then list your bot on Discordfinder and browse the utility bots already listed to see how established bots describe their commands.