By Sagar Shankaran, Founder of CallSphere
Build an AI-powered Discord bot that acts as a server assistant using TypeScript. Covers discord.js setup, slash command registration, conversation context management, tool integration, and permission-based access control.
Key takeaways
Discord provides a real-time messaging platform with built-in user identity, permissions, channels, and threads. These primitives map directly to agent concepts: users become agent clients, channels become conversation contexts, threads become persistent sessions, and server roles become permission boundaries.
Building an AI agent as a Discord bot gives you a production-ready interface without building a custom frontend — your users interact through a platform they already use daily.
Initialize a TypeScript project with discord.js and the OpenAI SDK:
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
mkdir discord-ai-agent && cd discord-ai-agent
npm init -y
npm install discord.js openai dotenv
npm install -D typescript @types/node tsx
npx tsc --init
Configure your environment:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
# .env
DISCORD_TOKEN=your-bot-token
DISCORD_CLIENT_ID=your-client-id
OPENAI_API_KEY=sk-proj-your-key
Create the bot client with the necessary intents:
// src/bot.ts
import { Client, GatewayIntentBits, Events } from "discord.js";
import { config } from "dotenv";
config();
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
client.once(Events.ClientReady, (readyClient) => {
console.log(`Bot ready as ${readyClient.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);
Discord's slash command system provides a structured interface for agent interactions:
// src/commands/register.ts
import { REST, Routes, SlashCommandBuilder } from "discord.js";
const commands = [
new SlashCommandBuilder()
.setName("ask")
.setDescription("Ask the AI assistant a question")
.addStringOption((opt) =>
opt
.setName("question")
.setDescription("Your question")
.setRequired(true)
),
new SlashCommandBuilder()
.setName("summarize")
.setDescription("Summarize recent messages in this channel")
.addIntegerOption((opt) =>
opt
.setName("count")
.setDescription("Number of messages to summarize")
.setMinValue(5)
.setMaxValue(100)
.setRequired(false)
),
new SlashCommandBuilder()
.setName("research")
.setDescription("Research a topic using multiple sources")
.addStringOption((opt) =>
opt.setName("topic").setDescription("Topic to research").setRequired(true)
),
];
const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
await rest.put(
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID!),
{ body: commands.map((c) => c.toJSON()) }
);
Connect slash commands to your AI agent:
// src/handlers/ask.ts
import { ChatInputCommandInteraction } from "discord.js";
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function handleAsk(interaction: ChatInputCommandInteraction) {
const question = interaction.options.getString("question", true);
// Defer reply since LLM calls take time
await interaction.deferReply();
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: `You are a helpful assistant in a Discord server.
Keep responses under 2000 characters (Discord's message limit).
Use markdown formatting that Discord supports.
Be concise and direct.`,
},
{ role: "user", content: question },
],
max_tokens: 1024,
});
const reply = completion.choices[0].message.content ?? "No response generated.";
await interaction.editReply(reply);
}
Register the handler in your main bot file:
// src/bot.ts
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
switch (interaction.commandName) {
case "ask":
await handleAsk(interaction);
break;
case "summarize":
await handleSummarize(interaction);
break;
case "research":
await handleResearch(interaction);
break;
}
});
Use Discord threads to maintain multi-turn conversations:
// src/handlers/conversation.ts
import { Message, ThreadChannel } from "discord.js";
const conversationHistory = new Map<string, OpenAI.ChatCompletionMessageParam[]>();
export async function handleThreadMessage(message: Message) {
if (message.author.bot) return;
if (!(message.channel instanceof ThreadChannel)) return;
const threadId = message.channel.id;
// Initialize or retrieve conversation history
if (!conversationHistory.has(threadId)) {
conversationHistory.set(threadId, [
{
role: "system",
content: "You are a helpful assistant in a Discord thread. Maintain context across messages.",
},
]);
}
const history = conversationHistory.get(threadId)!;
history.push({ role: "user", content: message.content });
// Trim history to last 20 messages to stay within token limits
const trimmed = [history[0], ...history.slice(-20)];
await message.channel.sendTyping();
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: trimmed,
});
const reply = completion.choices[0].message.content ?? "...";
history.push({ role: "assistant", content: reply });
// Discord has a 2000 character limit
if (reply.length > 2000) {
const chunks = reply.match(/.{1,2000}/gs) ?? [];
for (const chunk of chunks) {
await message.reply(chunk);
}
} else {
await message.reply(reply);
}
}
Build a tool that summarizes recent channel activity:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
// src/handlers/summarize.ts
export async function handleSummarize(
interaction: ChatInputCommandInteraction
) {
const count = interaction.options.getInteger("count") ?? 50;
await interaction.deferReply();
// Fetch recent messages
const messages = await interaction.channel?.messages.fetch({ limit: count });
if (!messages || messages.size === 0) {
await interaction.editReply("No messages found to summarize.");
return;
}
const transcript = messages
.reverse()
.map((m) => `${m.author.displayName}: ${m.content}`)
.join("\n");
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: "Summarize the following Discord conversation. Highlight key topics, decisions, and action items.",
},
{ role: "user", content: transcript },
],
});
await interaction.editReply(
completion.choices[0].message.content ?? "Could not generate summary."
);
}
Restrict agent commands based on Discord server roles:
function requireRole(roleName: string) {
return async (interaction: ChatInputCommandInteraction): Promise<boolean> => {
const member = interaction.member;
if (!member || !("roles" in member)) {
await interaction.reply({
content: "Could not verify your permissions.",
ephemeral: true,
});
return false;
}
const hasRole = member.roles.cache.some((r) => r.name === roleName);
if (!hasRole) {
await interaction.reply({
content: `You need the "${roleName}" role to use this command.`,
ephemeral: true,
});
return false;
}
return true;
};
}
// Usage in command handler
const checkAdmin = requireRole("AI Admin");
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === "research") {
if (!(await checkAdmin(interaction))) return;
await handleResearch(interaction);
}
});
Always call interaction.deferReply() immediately when handling a slash command. This gives you up to 15 minutes to send the actual response via interaction.editReply(). Without deferring, Discord expects a response within 3 seconds, which is too short for most LLM calls.
Check message.author.bot at the beginning of every message handler and return early if true. This prevents infinite loops where the bot triggers itself. Also check message.author.id !== client.user?.id for extra safety.
For production bots serving many servers, replace the in-memory Map with Redis or a database. Use the thread ID or channel ID as the key. Set a TTL (time to live) on conversations so they are automatically cleaned up after inactivity. Consider storing only the last N messages per thread to bound memory usage.
#Discord #Bot #TypeScript #AIAgent #Discordjs #SlashCommands #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.
Mastra.ai is becoming the go-to TypeScript agent framework in 2026. Workflows, RAG, evals, and an honest comparison with Vercel AI SDK 5 for serious teams.
Enterprise CIO Guide perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
SMB Founder Playbook perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Healthcare Practice Use Case perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Real Estate and Property Management Lens perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI