Your first bot

This walks you from nothing to a running bot that replies to a slash command, explained line by line. It assumes you have done Installing Node and your tools.

This walks you from nothing to a running bot that replies to a slash command, explained line by line. It assumes you have done Installing Node and your tools.

1. Create a Discord application and bot

  1. Go to the Discord Developer Portal.
  2. Click New Application, give it a name.
  3. Open the Bot tab. Click Reset Token and copy the token. Treat it like a password: anyone with it controls your bot. Never commit it or share it.
  4. Still on the Bot tab, scroll to Privileged Gateway Intents. For this tutorial you do not need any of them on.
  5. Open the OAuth2 -> URL Generator. Tick bot and applications.commands. Copy the generated URL, open it, and invite the bot to a server you own.

2. Store your token safely

In your project folder create a file named .env:

DISCORD_TOKEN=paste-your-token-here

Create another file named .gitignore so you never accidentally upload secrets or junk:

node_modules
.env
dist

Install a tiny helper to read the .env file:

npm install dotenv

3. Write the bot

Create src/index.ts:

import 'dotenv/config';
import { CommandClient, GatewayIntentBits } from 'athena';
import PingCommand from './commands/ping';
 
const client = new CommandClient({
  token: `Bot ${process.env.DISCORD_TOKEN}`,
  options: { intents: [GatewayIntentBits.Guilds] }
});
 
client.registerCommand(new PingCommand());
 
client.on('ready', async () => {
  console.log(`Logged in as ${client.user.username}`);
  await client.deployCommands();
});
 
client.on('error', (err) => console.error(err));
 
void client.connect();

Line by line:

  • import 'dotenv/config' reads .env so process.env.DISCORD_TOKEN works.
  • CommandClient is Athena's helper for slash commands. The token must start with Bot .
  • intents lists which events you want. Guilds is enough for slash commands. See Intents.
  • registerCommand adds a command (we write it next).
  • On ready, we tell Discord about our commands with deployCommands().
  • Always listen for error so problems are visible.
  • connect() logs the bot in.

4. Write the command

Create src/commands/ping.ts:

import { Command, CommandBuilder, SlashCommand } from 'athena';
import type { CommandInteraction } from 'athena';
 
@SlashCommand(new CommandBuilder('ping', 'Replies with pong'))
export default class PingCommand extends Command {
  async handleCommand(context: unknown, interaction: CommandInteraction) {
    await interaction.createMessage({ content: 'Pong!' });
  }
}
  • @SlashCommand(...) attaches the command's name and description.
  • CommandBuilder('ping', 'Replies with pong') defines a /ping command.
  • handleCommand runs when someone uses /ping. It replies with "Pong!".

If the decorator line shows an error in your editor, enable decorators by adding "experimentalDecorators": true to the compilerOptions in tsconfig.json.

5. Run it

For instant command updates while developing, point Athena at one test server. Find your server ID (enable Developer Mode in Discord settings, right click the server, Copy Server ID), then run:

DEV_GUILD=your-server-id npx tsx src/index.ts

You should see "Logged in as ...". In Discord, type / in your test server and you will see ping. Run it; the bot replies "Pong!".

If /ping does not appear, give it a moment and retype /. Guild commands (from DEV_GUILD) appear within seconds; global commands can take up to an hour, which is why we use a dev guild while building.

What just happened

  • CommandClient connected to Discord's gateway (a live websocket) and logged in.
  • deployCommands() registered /ping with Discord.
  • When you ran /ping, Discord sent your bot an interaction, Athena turned it into a CommandInteraction, found your PingCommand, and called handleCommand.

Next steps