Node Discord bot

A Discord bot answers uptime questions, rolls familiar dice, and gives playful 8ball replies inside a server you manage. Tokay keeps its outbound Discord connection running as a Background Worker with logs beside the Service.

Last updated

This is the useful shape for a bot that waits on a provider connection instead of serving a web page. You bring a Discord bot token, and Tokay keeps the process alive and makes connection failures visible.

Runtime
Node.js
Framework
discord.js
Service types
Background Worker
Resources
None
Required secrets
DISCORD_TOKEN

What it does

Once Logs shows Campfire ready, the bot registers three slash commands in your Discord server.

Use /uptime to see when the current process started. Ask /8ball a question for a random campfire answer, or choose a die from d4 through d100 with /roll.

When the Worker restarts, Discord reconnects and the commands register again. The next uptime response reflects the new process.

Deploy it

Bring this folder to Tokay any way you like. Paste or upload it in the dashboard, push it with git, or hand it to your AI agent along with our agent instructions. Tokay figures out the rest.

Working in Claude, ChatGPT, VS Code, or another MCP client? Connect the Tokay MCP server and ask your agent to deploy this example. It signs in through your browser, so there is no token to paste.

New to Tokay? The getting started guide walks you through your first deploy.

Set up Discord first

Create the bot in Discord before you deploy it. You will receive one token that lets this app connect to your server. Keep that token out of this folder and out of chat.

  1. Open the Discord Developer Portal and choose New Application.
  2. Name the app, create it, then open Bot in the left sidebar.
  3. Under Token, choose Reset Token and copy the value. Discord only shows it once.
  4. Open Installation. Under Install Link, keep Discord Provided Link.
  5. Under Default Install Settings for Guild Install, add the applications.commands and bot scopes. Give the bot Send Messages permission.
  6. Copy the install link, open it, choose Add to server, and select a test server you manage.

These commands do not need privileged intents. If a Discord screen looks different, the official Discord guide covers the current token and installation flow.

Add the Discord token

Open Environment and find DISCORD_TOKEN under From your code. Choose Set shared value, paste the bot token, and save it.

When the row says Good to go, follow the deployment tracker and choose Go Live if that action appears. The Background Worker has no public page. Logs will show when the bot is connected.

Try it

  1. Wait for Campfire ready in Logs, then run /roll die: d20 in Discord.
  2. Run /uptime and note the reported start time.
  3. Restart the Worker and ask again. The command returns after reconnection with a new start time.

If the token is wrong, Logs shows a direct Discord login error. Tokay restarts the Background Worker and makes the repeated failure visible as an incident. Replace DISCORD_TOKEN with a valid value and the bot will reconnect.

How it works

You do not need a public callback URL because discord.js opens one outbound gateway connection. The ready event registers the commands for every installed server.

Tokay sends shutdown signals during a deployment, and the bot closes its Discord client cleanly. Reconnects, errors, and command registration remain visible in Logs when you need to trace a problem.

Secrets

Name Required Purpose
DISCORD_TOKEN Yes Authenticates this bot with Discord. Save it under the Project's encrypted Secrets.

Grow from here

Try the statement generator when you want an always running worker that pulls saved jobs from Redis.

bot.js Raw

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

const token = process.env.DISCORD_TOKEN;
if (!token) {
  console.error('DISCORD_TOKEN is missing. Set the Project Secret before deploying this bot.');
  process.exit(1);
}

const startedAt = new Date();
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const commands = [
  new SlashCommandBuilder()
    .setName('uptime')
    .setDescription('See how long Campfire has been awake'),
  new SlashCommandBuilder()
    .setName('8ball')
    .setDescription('Ask the campfire a question')
    .addStringOption(option => option.setName('question').setDescription('What is on your mind?').setRequired(true)),
  new SlashCommandBuilder()
    .setName('roll')
    .setDescription('Roll one familiar die')
    .addStringOption(option => option
      .setName('die')
      .setDescription('Choose a die')
      .addChoices(
        { name: 'd4', value: 'd4' },
        { name: 'd6', value: 'd6' },
        { name: 'd8', value: 'd8' },
        { name: 'd10', value: 'd10' },
        { name: 'd12', value: 'd12' },
        { name: 'd20', value: 'd20' },
        { name: 'd100', value: 'd100' },
      )),
].map(command => command.toJSON());

const answers = [
  'The sparks say yes.',
  'Give it one more night.',
  'Yes, and bring snacks.',
  'The smoke is undecided.',
  'Not this time. Keep the good idea nearby.',
  'Ask again after a brave first step.',
  'Absolutely. The kettle is already on.',
  'The logs crackled no.',
];

const duration = (milliseconds) => {
  const totalSeconds = Math.floor(milliseconds / 1000);
  const days = Math.floor(totalSeconds / 86400);
  const hours = Math.floor((totalSeconds % 86400) / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  return [days && `${days}d`, hours && `${hours}h`, `${minutes}m`].filter(Boolean).join(' ');
};

const registerGuild = async (guild) => {
  await guild.commands.set(commands);
  console.log(`Registered ${commands.length} commands in ${guild.name}.`);
};

client.once(Events.ClientReady, async (readyClient) => {
  console.log(`Campfire ready as ${readyClient.user.tag}.`);
  await Promise.all(readyClient.guilds.cache.map(registerGuild));
  if (readyClient.guilds.cache.size === 0) {
    await readyClient.application.commands.set(commands);
    console.log('Registered global commands. Install the app in a server to use it there.');
  }

  if (process.env.DEV_CRASH === '1') {
    console.warn('DEV_CRASH is set. Throwing the requested proof crash in five seconds.');
    setTimeout(() => { throw new Error('Requested DEV_CRASH proof'); }, 5000);
  }
});

client.on(Events.GuildCreate, guild => {
  registerGuild(guild).catch(error => console.error(`Could not register commands in ${guild.name}.`, error.message));
});
client.on(Events.ShardReconnecting, shardId => console.log(`Discord shard ${shardId} reconnecting.`));
client.on(Events.ShardResume, (shardId, replayed) => console.log(`Discord shard ${shardId} resumed after replaying ${replayed} event(s).`));
client.on(Events.Error, error => console.error('Discord client error.', error.message));

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

  if (interaction.commandName === 'uptime') {
    await interaction.reply(`Campfire has been awake for ${duration(Date.now() - startedAt.getTime())}. On Tokay since ${startedAt.toISOString()}.`);
    return;
  }
  if (interaction.commandName === '8ball') {
    await interaction.reply(answers[Math.floor(Math.random() * answers.length)]);
    return;
  }
  if (interaction.commandName === 'roll') {
    const die = interaction.options.getString('die') || 'd20';
    const sides = Number(die.slice(1));
    const result = Math.floor(Math.random() * sides) + 1;
    await interaction.reply(`You rolled **${result}** on a ${die}.`);
  }
});

client.login(token).catch((error) => {
  console.error('Discord login failed. Check the DISCORD_TOKEN Project Secret.', error.message);
  process.exit(1);
});

const shutdown = async (signal) => {
  console.log(`${signal} received. Closing the Discord connection.`);
  client.destroy();
  process.exit(0);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

package.json Raw

{
  "name": "campfire-discord-bot",
  "version": "1.0.0",
  "private": true,
  "engines": {
    "node": ">=24"
  },
  "scripts": {
    "start": "node bot.js"
  },
  "dependencies": {
    "discord.js": "^14.27.0"
  }
}

Built from revision 52c7b7f

Guide: Host a Discord bot for free