Deploy your own AI chatbot using Letta to create agents that can learn over time.
Note
You must also have a Discord app to use this app. Follow these instructions to create your Discord app.
-
🧠 Letta
- Formerly known as MemGPT, Letta is an open-source framework designed for building stateful LLM applications. Our Discord bot example showcases powerful core features of Letta.
-
Discord Bot
-
- The Letta TypeScript library provides convenient access to the Letta API.
-
- Discord.js is a Node.js library that allows you to interact with the Discord API, making it easy to build bot applications.
-
- Express JS is a minimal and flexible web framework for Node.js. We use Express to create a web server that accepts HTTP requests and interacts with the Letta server to generate responses. Express is also used to interact with the Discord API.
-
- TypeScript enhances our codebase with static typing, improved maintainability, and better developer tooling, reducing potential runtime errors.
# Install dependencies
npm install
# Run in development mode (with auto-reload)
npm run dev
# Run in production mode
npm start
# Build TypeScript to JavaScript
npm run build- Discord message received and filtered based on type and configuration
- Conversation history fetched from channel (last N messages, configurable)
- Message formatted with sender context and channel info, sent to Letta agent
- Letta streams response chunks back
- Response sent to Discord (auto-split if longer than 2000 characters)
Note
These are instructions for running the Discord bot server locally, which connects a Letta server to Discord. If you're using Letta Cloud, all you'll need is your Letta Cloud API key + the Discord bot server, but if you're self-hosting, you'll also need to set up a Letta server.
Follow the quickstart guide to get your own Letta Cloud API key.
You can run your own Letta server using Letta Desktop or Docker.
If you're self-hosting a server, the Letta server will run on http://localhost:8283 by default (that will be your LETTA_BASE_URL).
1️⃣ Clone the repository and install dependencies:
# Clone the repository
git clone https://github.com/letta-ai/letta-discord-bot-example.git
# Navigate to the project directory
cd letta-discord-bot-example
# Install dependencies
npm install
# Set environment variables
cp .env.template .env2️⃣ Update the .env file with your Letta variables
1️⃣ Create a new Discord application here.
2️⃣ Under Settings -> General Information of your Discord app, copy your Discord application's Application ID and Public Key, and paste them in your .env file.
3️⃣ Under Settings -> Bot of your Discord app, copy your Discord bot's Token, and paste it in your .env file.
4️⃣ Enable the Privileged Gateway Intents
5️⃣ Under Settings -> Installation, under Guild Install set up scopes and permissions
6️⃣ Install Discord Bot on your server; copy and paste Link on your browser.
Environment variables can be controlled by setting them in your .env file or by setting them in your deployment environment.
| Variable | Description | Default |
|---|---|---|
LETTA_API_KEY |
API key for Letta Cloud, or password if self-hosting with authentication | - |
LETTA_BASE_URL |
Base URL of your Letta server | https://api.letta.com |
LETTA_AGENT_ID |
ID of the Letta agent to use | Required |
LETTA_USE_SENDER_PREFIX |
Include sender context prefix on messages | true |
| Variable | Description | Default |
|---|---|---|
LETTA_CONTEXT_MESSAGE_COUNT |
Number of recent messages to include as context (0 to disable) | 5 |
LETTA_THREAD_CONTEXT_ENABLED |
Fetch full thread context when in a thread | true |
LETTA_THREAD_MESSAGE_LIMIT |
Max messages to fetch from threads (0 for unlimited) | 50 |
| Variable | Description | Default |
|---|---|---|
APP_ID |
Discord application ID | Required |
DISCORD_TOKEN |
Discord bot token | Required |
PUBLIC_KEY |
Discord application public key | Required |
DISCORD_CHANNEL_ID |
Only listen to messages in this channel | - |
DISCORD_RESPONSE_CHANNEL_ID |
Only respond in this channel (agent sees all) | - |
| Variable | Description | Default |
|---|---|---|
RESPOND_TO_DMS |
Respond to direct messages | true |
RESPOND_TO_MENTIONS |
Respond to @mentions | true |
RESPOND_TO_BOTS |
Respond to other bots | false |
RESPOND_TO_GENERIC |
Respond to all channel messages | false |
SURFACE_ERRORS |
Show errors in Discord (vs logs only) | false |
| Variable | Description | Default |
|---|---|---|
ENABLE_TIMER |
Enable periodic heartbeat events | true |
TIMER_INTERVAL_MINUTES |
Max interval for random timer | 15 |
FIRING_PROBABILITY |
Probability timer fires (0.0-1.0) | 0.1 |
Note: Timer requires
DISCORD_CHANNEL_IDto be set.
| Variable | Description | Default |
|---|---|---|
MESSAGE_BATCH_ENABLED |
Accumulate messages before sending to agent | false |
MESSAGE_BATCH_SIZE |
Max messages per batch | 10 |
MESSAGE_BATCH_TIMEOUT_MS |
Auto-drain timeout | 30000 |
| Variable | Description | Default |
|---|---|---|
PORT |
Port to run the app on | 3001 |
You can connect an existing agent to Discord (by using its LETTA_AGENT_ID), or you can create a brand new agent specifically to use as a Discord bot.
If you create a new agent, we'd recommend adding some information (e.g. inside of the human or persona memory block) that explains how to interact with Discord. For example, placing the following text in human:
I can use this space in my core memory to take notes on the users that I am interacting with.
So far, all I know that is that I am connected to a Discord server.
I can see messages that other users send on this server, as long as they are directed at me (with a mention or a reply).
I should also remember that if I want to "at" a user, I need to use the <@discord-id> format in my message response.
This will render the user tag in a dynamic way on Discord, vs any other reference to a user (eg their username) will just result in plaintext.
Additionally, if you would like to give your chatbot/agent the ability to "ignore" (not reply) to certain messages, you can add a custom tool like this to your agent (for information on how to add a custom tool, see our docs):
def ignore():
"""
Not every message warrants a reply (especially if the message isn't directed at you). Call this tool to ignore the message.
"""
returnThe ability for an agent to "ignore" messages can be crucial if you connect your agent to an active Discord channel with many participants, especially if you set RESPOND_TO_GENERIC to true (in which case the agent will "see" every single message in a channel, even messages not directed at the agent itself).
To run the app locally, simply do:
npm startThis will spin up the Discord bot service, which will listen for events on Discord, and when an event happens (e.g. a message is sent in a channel), it will send an appropriate message to the Letta server, check for a response from the Letta server, and potentially send back a reply message on Discord.
We have also prepared a one-click deploy option to easily deploy this repo on Railway. Simply click the deploy link, enter your environment variables (including your Letta server address and Letta agent ID), and your Discord bot will be ready to go (and live 24/7):
The bot distinguishes between four message types, each with a different prefix format sent to the agent:
| Type | When it applies | Format |
|---|---|---|
| DM | Direct message to the bot | [username (id=123) sent you a direct message] message |
| MENTION | Message @mentions the bot | [username (id=123) sent a message in #channel mentioning you] message |
| REPLY | Reply to bot's previous message | [username (id=123) replied to you in #channel] message |
| GENERIC | Other channel messages | [username (id=123) sent a message in #channel] message |
This context helps the agent understand where messages come from and respond appropriately.
The bot includes recent message history as context for the agent:
Regular channels:
- Fetches the last N messages (configured via
LETTA_CONTEXT_MESSAGE_COUNT) - Includes both user and bot messages
- Filters out command messages (starting with
!) - Format:
[Recent conversation context:] - username1: message text - username2: message text [End context] [Current message from username]
Threads:
- Automatically detects thread messages
- Fetches thread starter and all replies (up to
LETTA_THREAD_MESSAGE_LIMIT) - Format:
[Thread: "Thread name"] [Thread started by username: "original message"] [Thread conversation history:] - user1: message - user2: reply [End thread context] [Current message from user]
Thread context takes precedence over regular conversation history when in a thread.
Auto-splitting: Messages longer than Discord's 2000 character limit are automatically split into multiple messages.
Code block preservation: When splitting, the bot preserves markdown code blocks, ensuring they aren't broken across messages.
Code block isolation: Code blocks are sent as separate messages for easy copying.
When enabled, messages are accumulated before sending to the agent:
- Each channel has its own message buffer
- Batch drains when reaching
MESSAGE_BATCH_SIZEorMESSAGE_BATCH_TIMEOUT_MS - Format:
[Batch of 5 messages from #general] 1. [username (id=123) mentioned you] message text 2. [username2 (id=456)] another message ...
This reduces API calls and provides better conversation context for active channels.
When enabled, the bot sends periodic heartbeat events to the agent:
- Fires at random intervals between 1 minute and
TIMER_INTERVAL_MINUTES - Only fires based on
FIRING_PROBABILITY(default 10%) - Requires
DISCORD_CHANNEL_IDto know where to send responses - Allows the agent to initiate conversations or update memory autonomously
