Guides12 min readPublished 2026-08-03

How to Build a Discord AI Bot with Node.js and LumeAPI

Build a Discord AI bot with a secure slash command, immediate deferred replies, an OpenAI-compatible model call, concurrency limits and error handling.

By LumeAPI Engineering Team

Cheap LLM API hub → Build with a Chatbot API →

Short path: Chatbot API · Multi-model API · OpenAI-compatible API · Production reliability

Last verified: August 3, 2026

The safest beginner architecture for a Discord AI bot is a slash command handled on your server: Discord sends /ask, your code immediately defers the reply, calls an OpenAI-compatible API and edits the deferred response. The Discord token and LumeAPI key remain server-side, and the bot does not need permission to read every message in a server.

This Node.js example uses discord.js plus the OpenAI JavaScript SDK pointed at LumeAPI.

What you will build

text
Discord user runs /ask
        ↓
discord.js receives the interaction
        ↓
Bot defers within Discord's deadline
        ↓
Server calls LumeAPI /v1/chat/completions
        ↓
Bot edits the deferred reply

Discord requires an initial interaction response within three seconds. Model calls can take longer, so deferReply() is essential.

Create the Discord application

In the Discord Developer Portal:

  1. Create an application.
  2. Open Bot and create or reset the bot token.
  3. Add the application to a test server with the bot and applications.commands scopes.
  4. Grant only the permissions the bot needs to reply.
  5. Copy the Application ID and test server Guild ID.

Treat the bot token as a password. Never put it in browser code, a Git repository, a screenshot or the slash-command payload.

Install the project

Use a current supported Node.js release, then create the project:

bash
npm init -y
npm install discord.js openai dotenv

Add "type": "module" to package.json. Create .env locally:

text
DISCORD_TOKEN=replace_me
DISCORD_CLIENT_ID=replace_me
DISCORD_GUILD_ID=replace_me
LUMEAPI_API_KEY=replace_me

Add .env to .gitignore before the first commit.

Register the /ask command

Create register.js:

javascript
import "dotenv/config";
import { REST, Routes, SlashCommandBuilder } from "discord.js";

const command = new SlashCommandBuilder()
  .setName("ask")
  .setDescription("Ask the server AI assistant a question")
  .addStringOption((option) =>
    option
      .setName("question")
      .setDescription("The question to answer")
      .setRequired(true)
      .setMaxLength(1800)
  );

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

await rest.put(
  Routes.applicationGuildCommands(
    process.env.DISCORD_CLIENT_ID,
    process.env.DISCORD_GUILD_ID
  ),
  { body: [command.toJSON()] }
);

console.log("Registered /ask in the test server.");

Registering a guild command is useful during development because changes propagate faster than global commands:

bash
node register.js

Call LumeAPI from the bot

Create bot.js:

javascript
import "dotenv/config";
import { Client, Events, GatewayIntentBits } from "discord.js";
import OpenAI from "openai";

const required = ["DISCORD_TOKEN", "LUMEAPI_API_KEY"];
for (const name of required) {
  if (!process.env[name]) throw new Error(`Missing ${name}`);
}

const discord = new Client({ intents: [GatewayIntentBits.Guilds] });
const ai = new OpenAI({
  apiKey: process.env.LUMEAPI_API_KEY,
  baseURL: "https://api.lumeapi.site/v1",
  timeout: 45_000,
  maxRetries: 2,
});

const activeUsers = new Set();

discord.once(Events.ClientReady, (client) => {
  console.log(`Ready as ${client.user.tag}`);
});

discord.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== "ask") return;

  if (activeUsers.has(interaction.user.id)) {
    await interaction.reply({ content: "Your previous request is still running.", ephemeral: true });
    return;
  }

  activeUsers.add(interaction.user.id);
  await interaction.deferReply();

  try {
    const question = interaction.options.getString("question", true).trim();
    const result = await ai.chat.completions.create({
      model: "gpt-5.4-mini",
      messages: [
        {
          role: "system",
          content: "Answer helpfully and concisely. Do not claim private server knowledge."
        },
        { role: "user", content: question }
      ],
      temperature: 0.2,
    });

    const answer = result.choices[0]?.message?.content?.trim();
    if (!answer) throw new Error("The model returned no text.");

    await interaction.editReply(answer.slice(0, 1900));
  } catch (error) {
    console.error("ask failed", error);
    await interaction.editReply("The AI request failed. Please try again later.");
  } finally {
    activeUsers.delete(interaction.user.id);
  }
});

await discord.login(process.env.DISCORD_TOKEN);

Run it:

bash
node bot.js

In the test server, run /ask question: Reply with connection ok.

Why slash commands are better than reading every message

A message-listening bot often requires the privileged Message Content intent and receives far more data than an explicit command needs. Slash commands reduce accidental collection, give users a visible input schema and make rate controls easier.

If your real use case requires mentions or support-channel messages, document exactly which channels are read and how long content is retained. Do not silently send private or staff-only conversations to a model provider.

Production controls users usually miss

The example includes one active request per user, but production needs more:

  • Add global and per-guild concurrency limits.
  • Cap prompt length and estimated tokens.
  • Reject unsupported file or URL input instead of pretending to read it.
  • Split replies safely when output can exceed Discord's message limit.
  • Keep a model allowlist.
  • Record request ID, guild ID hash, latency, tokens and outcome without logging secrets.
  • Add moderation and server policy appropriate to the community.
  • Require explicit authorization before tools can change external systems.

Discord's HTTP rate limits can change and should not be hard-coded. discord.js manages Discord API rate limits, while your own model-call queue must separately handle 429 and upstream failures.

Common failures

SymptomCauseFix
“The application did not respond”No initial reply within three secondsCall deferReply() before the model request
Slash command missingCommand not registered in the current guildCheck Application ID, Guild ID and registration output
401 from LumeAPIInvalid inference keyCheck the server environment; never paste it into Discord
404 from LumeAPIWrong model or endpointVerify /v1 and the model catalog
Duplicate or slow repliesMultiple listeners or unbounded concurrencyRun one process during testing and add a shared queue in production
Reply too longDiscord message limitTruncate or split at safe boundaries

Where to take the bot next

For server support, add retrieval from approved FAQs instead of stuffing entire chat histories into every prompt. For multiple models, route simple questions to a low-cost model and escalate only difficult tasks through the multi-model API. Measure cost per resolved question and human escalation rate.

Sources and verification boundary

The JavaScript was syntax-checked on August 3, 2026. A Discord account, server and inference key are required for the end-to-end test, so production permission and billing behavior must be verified in your own test guild.

Ready to call these models?

Create a LumeAPI key in under a minute — one OpenAI-compatible gateway for GPT, Claude, Gemini, and more.