Your first bot
Create a seedcord project with the scaffolder and get it answering in Discord. Covers what it asks, running it, what the scaffolder made, changing the ping command, and a file that fails to import.
You need the token from the last page.
pnpm create seedcordWhat it asks
- Where the project goes. The folder name becomes the package name, so it takes lowercase letters, digits, and
. _ -. - TypeScript or JavaScript.
- How Discord reaches your bot. Gateway or http, the choice from two pages ago.
- What your bot should react to. Gateway only. A checklist covering messages in servers, reactions, member joins, polls, and more. Each one says which intents and partials it needs, and the privileged ones are marked. The scaffolder writes the set into your
bot.ts. - Your bot token.
- Your app public key. Http only.
- Your bot color. Every embed and container you build takes it, unless you set a color on that one yourself.
Then the scaffolder installs your dependencies, formats the project, runs seedcord codegen, and makes a git commit.
Run it
cd my-bot
pnpm run devYour bot logs in and sends its commands to Discord. The terminal turns into a dev console.
Run the ping command
The scaffold ships a ping command. Type /ping in a server your bot is in.
The command probably won't be there yet. Global commands take time to appear, and Discord doesn't publish how long that takes.
Tip
Restart your Discord client with Ctrl+R or Cmd+R. That fetches a fresh command list and usually ends the wait.
What the scaffolder made
The ping command came from a file the scaffolder wrote, along with everything else in this tree.
my-bot/
├─ .vscode/
│ └─ extensions.json
├─ src/
│ ├─ commands/
│ │ └─ Ping.ts
│ ├─ events/
│ │ └─ Ready.ts
│ ├─ handlers/
│ │ └─ Ping.ts
│ ├─ bot.ts
│ ├─ index.ts
│ └─ seedcord-gen.d.ts
├─ .env
├─ .gitignore
├─ .prettierignore
├─ eslint.config.ts
├─ package.json
├─ prettier.config.ts
├─ README.md
├─ seedcord.config.ts
├─ tsconfig.build.json
└─ tsconfig.jsonsrc/bot.tsbuilds your bot and exports it.src/index.tsimports that and starts it. They are separate so the CLI can importbot.tsand read your commands without logging in.seedcord.config.tsconfigures the CLI. Your bot's own settings go inbot.ts, which is the file below.src/seedcord-gen.d.tscarries the types generated from your config and your commands.seedcord codegenwrites it and you commit it.tsconfig.jsontype-checks the project.tsconfig.build.jsoncompilessrcintodist..envholds your token.bot.tssetsEnvapter.baseDirto the project root, which is where envapt (opens in a new tab) then looks for the file.src/events/holdsReady.ts. Picking any of the message capabilities swaps that forMentioned.ts, which answers when someone mentions your bot.
Http only
Discord posts only interactions to an http bot, so the
scaffolder writes no src/events folder.
Your bot's settings
Every answer you gave shows up in one object.
import { resolve } from 'node:path';
import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';
import { Envapter } from 'envapt';
Envapter.baseDir = resolve(import.meta.dirname, '..');
export const seedcord = new Seedcord({
bot: {
// passed straight to the discord.js Client constructor
clientOptions: {
intents: [GatewayIntentBits.Guilds]
},
interactions: {
path: resolve(import.meta.dirname, './handlers')
},
commands: {
path: resolve(import.meta.dirname, './commands')
},
events: {
path: resolve(import.meta.dirname, './events')
}
},
subscribers: {
path: null
},
botColor: '#4ade80',
notifications: {
developerUsername: 'you'
}
});
export default seedcord;The path keys name the folders seedcord scans at startup. The tab that teaches each folder covers its key.
Http only
An http config takes a top-level port, which defaults to 3000. clientOptions and events belong to gateway alone.
The ping command
Two files make /ping work.
import {
BuilderComponent,
RegisterCommand
} from '@seedcord/gateway';
@RegisterCommand('global')
export class Ping extends BuilderComponent<'command'> {
constructor() {
super('command');
this.instance
.setName('ping')
.setDescription('Check that the bot is answering')
.addBooleanOption((option) =>
option
.setName('detailed')
.setDescription('Include process uptime')
);
}
}Discord gets a name, a description, and one optional boolean.
import {
SlashHandler,
SlashRoute,
timestampFromSnowflake
} from '@seedcord/gateway';
@SlashRoute('ping')
export class Ping extends SlashHandler<'ping'> {
public async execute(): Promise<void> {
const roundtrip =
Date.now() - timestampFromSnowflake(this.event.id);
const lines = ['### Pong', `Roundtrip **${roundtrip}ms**`];
if (this.options.getBoolean('detailed')) {
lines.push(
`Uptime **${Math.round(process.uptime())}s**`
);
}
await this.reply(lines.join('\n'));
}
}That runs when someone uses it. 'ping' appears twice, on the decorator and on the generic, and both have to match the name the command sets. The compiler catches a mismatch. this.options carries detailed because seedcord codegen recorded it off the command file.
Http only
An http scaffold writes the same two files and imports from
@seedcord/http.
Change something
Open src/handlers/Ping.ts and change what it replies with. Save.
The dev server reloads the handler while the bot stays connected. Run /ping again and the new reply comes back.
Now open src/commands/Ping.ts and change the command itself, its description for example. Save, then look at the terminal. The dev server asks whether to re-register.
Commands updated:
- src/commands/Ping.ts
Refresh commands? (y/n)A handler edit needs no confirmation because nothing leaves your machine. A command edit does, since the new definition has to go to Discord before anyone sees it.
Press y. That sends the new command to Discord and runs seedcord codegen, which rewrites src/seedcord-gen.d.ts so your handler's types match what the command now declares.
Press n if you want Discord to keep the old command.
When a file fails to import
seedcord imports every file under src/commands, src/handlers, and src/events at startup. A syntax error in any one of them stops the whole scan. A half-typed file saved mid-edit does it too, so the restart below is the normal way back.
src/handlers/Ping.ts threw while importing.Fix the file and press r to restart.