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
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 replyDiscord 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:
- Create an application.
- Open Bot and create or reset the bot token.
- Add the application to a test server with the
botandapplications.commandsscopes. - Grant only the permissions the bot needs to reply.
- 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:
npm init -y
npm install discord.js openai dotenvAdd "type": "module" to package.json. Create .env locally:
DISCORD_TOKEN=replace_me
DISCORD_CLIENT_ID=replace_me
DISCORD_GUILD_ID=replace_me
LUMEAPI_API_KEY=replace_meAdd .env to .gitignore before the first commit.
Register the /ask command
Create register.js:
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:
node register.jsCall LumeAPI from the bot
Create bot.js:
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:
node bot.jsIn 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
| Symptom | Cause | Fix |
|---|---|---|
| “The application did not respond” | No initial reply within three seconds | Call deferReply() before the model request |
| Slash command missing | Command not registered in the current guild | Check Application ID, Guild ID and registration output |
401 from LumeAPI | Invalid inference key | Check the server environment; never paste it into Discord |
404 from LumeAPI | Wrong model or endpoint | Verify /v1 and the model catalog |
| Duplicate or slow replies | Multiple listeners or unbounded concurrency | Run one process during testing and add a shared queue in production |
| Reply too long | Discord message limit | Truncate 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
- Discord: building your first bot
- Discord: receiving and responding to interactions
- Discord API rate limits
- discord.js slash-command guide
- LumeAPI chatbot API
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.