This recipe wires your discord.js bot up to a large language model so that mentioning it — @YourBot what's a good pizza topping? — gets a real answer. It uses the Claude API as the example, keeps the API key in .env where it belongs, and is honest about the two things that trip people up: cost and safety.
| At a glance | |
|---|---|
| You need | A working bot from Host a discord.js bot, and an API key (below) |
| Plan | Free or premium — no premium features required |
| Time | About twenty minutes |
New here? Build the base bot with Host a discord.js bot first.
Get an API key
Sign up at the Anthropic Console and create an API key. It's a secret, like your bot token — treat it the same way.
💰 This costs money. Unlike the bot token, an API key bills you per request — you pay for the words in and the words out. Set a spend limit in the console before you wire it up, so a runaway bot can't run up a bill. There's no free lunch here; start small.
Turn on the Message Content intent
The bot has to read the message that mentions it, which needs the Message Content Intent — a privileged intent. On the Discord Developer Portal → your app → Bot, switch it on.
Add the package and the key
Open the Packages page in your server menu and install @anthropic-ai/sdk. Then add the key to your .env (the same file that holds your bot token):
DISCORD_TOKEN=your-bot-token
ANTHROPIC_API_KEY=your-api-key
🔑 Keep the key out of your code. Never paste it into
index.js, and never commit.envto Git — the starter.gitignorealready excludes it. See Environment variables & secrets. If a key ever leaks, rotate it in the console immediately.
The whole bot
Here's the complete index.js. The bot replies only when it's mentioned.
require('dotenv').config();
const { Client, GatewayIntentBits, Events } = require('discord.js');
const Anthropic = require('@anthropic-ai/sdk');
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
const SYSTEM_PROMPT =
'You are a friendly Discord bot. Keep replies short, helpful, and under 1500 characters.';
client.once(Events.ClientReady, (c) => console.log(`Listening as ${c.user.tag}`));
client.on(Events.MessageCreate, async (message) => {
if (message.author.bot) return;
if (!message.mentions.has(client.user)) return;
const prompt = message.content.replace(/<@!?\d+>/g, '').trim();
if (!prompt) return;
await message.channel.sendTyping();
try {
const reply = await anthropic.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: prompt }],
});
const text = reply.content.find((b) => b.type === 'text')?.text ?? '(no reply)';
await message.reply(text.slice(0, 2000));
} catch (err) {
console.error(err);
await message.reply('Sorry, I could not answer that right now.');
}
});
client.login(process.env.DISCORD_TOKEN);
Restart, then in a channel type @YourBot say hello. It should reply.
How it works
new Anthropic()readsANTHROPIC_API_KEYfrom the environment — the key never appears in your code.- Reply only on a mention.
message.mentions.has(client.user)gates every call. This is your first cost control: the bot ignores every message that isn't aimed at it. The mention is stripped out of the text before it's sent. messages.createsends the request: amodel, amax_tokenscap (an upper bound on the reply length — and the cost), asystemprompt that sets the bot's personality, and the user's message. The reply comes back as a list of content blocks; you pull the text out of the first text block.sendTyping()shows the "…is typing" indicator while the model thinks, so the wait feels intentional.
🎯 Good to know: The model here is
claude-sonnet-5— a good balance of quality and cost. You can swap it for another model by changing that one string; keep the rest the same.
Keep it safe and affordable
Two callouts that save real money and headaches:
⚠️ Every reply is a charge. Cap it:
max_tokenslimits the reply length; the mention gate limits when it runs. Add a per-user cooldown so one person can't spam requests — see Per-user cooldowns. Watch your usage in the console for the first few days.
🔒 This bot has no memory. Each message is answered on its own — the model doesn't see earlier messages. That keeps costs predictable. If you add conversation history (passing previous turns in
messages), remember every extra turn is more tokens, and therefore more cost, on every reply.
Prefer OpenAI or another provider?
The idea is the same everywhere. Many providers — including OpenAI itself and OpenAI-compatible services — expose a similar "send messages, get a reply" endpoint. You'd install that provider's package (for OpenAI, openai), point it at their base URL if needed, and call their equivalent of messages.create. The important parts don't change: keep the key in .env, cap the reply length, and gate on mentions. Pick whichever provider you have a key for.
Verify it works
Mention the bot with a question and confirm you get a sensible reply. Then check the console shows no errors, and glance at your provider's usage dashboard to confirm the request registered. If the bot replies "Sorry, I could not answer that right now," open the console — the real error is printed there.
Troubleshooting
| Symptom | Fix |
|---|---|
Console shows a 401 / authentication_error |
The ANTHROPIC_API_KEY in .env is missing or wrong. Copy a fresh key from the console. |
| Bot ignores the mention | The Message Content intent is off (the bot can't read the text), or you didn't actually @mention it. |
Used disallowed intents on start |
Enable Message Content in the Developer Portal Bot tab. |
Cannot find module '@anthropic-ai/sdk' |
Install it from the Packages page and restart. |
| Replies cut off | Raise max_tokens — but remember a higher cap means a higher possible cost per reply. |