Build event‑driven, middleware‑powered messaging agents on the XMTP network. 🚀
Full agent building guide: Build an XMTP Agent
This SDK is based on familiar Node.js patterns: you register event listeners, compose middleware, and extend behavior just like you would in frameworks such as Express. This makes it easy to bring existing JavaScript and TypeScript skills into building conversational agents.
Choose your package manager:
npm install @xmtp/agent-sdk
# or
pnpm add @xmtp/agent-sdk
# or
yarn add @xmtp/agent-sdkimport { Agent, createUser, createSigner, getTestUrl } from "@xmtp/agent-sdk";
// 1. Create a local user + signer (you can plug in your own wallet signer)
const user = createUser();
const signer = createSigner(user);
// 2. Spin up the agent
const agent = await Agent.create(signer, {
env: "dev", // or 'production'
dbPath: null, // in-memory store; provide a path to persist
});
// 3. Respond to text messages
agent.on("text", async (ctx) => {
await ctx.sendText("Hello from my XMTP Agent! 👋");
});
// 4. Log when we're ready
agent.on("start", (ctx) => {
console.log(`We are online: ${getTestUrl(ctx.client)}`);
});
await agent.start();The XMTP Agent SDK supports configuration through environment variables (process.env), making it easy to configure your agent without code changes. Set the following variables and call Agent.createFromEnv() to automatically load them:
Available Variables:
| Variable | Purpose | Example |
|---|---|---|
XMTP_DB_DIRECTORY |
Database directory | XMTP_DB_DIRECTORY=my/database/dir |
XMTP_DB_ENCRYPTION_KEY |
Database encryption key | XMTP_DB_ENCRYPTION_KEY=0xabcd...1234 |
XMTP_ENV |
Network environment | XMTP_ENV=dev or XMTP_ENV=production |
XMTP_WALLET_KEY |
Private key for Ethereum wallet | XMTP_WALLET_KEY=0x1234...abcd |
Using the environment variables, you can setup your agent in just a few lines of code:
// Load variables from .env file
process.loadEnvFile(".env");
// Create agent using environment variables
const agent = await Agent.createFromEnv();Agents can also recognize the following environment variables:
| Variable | Purpose | Example |
|---|---|---|
XMTP_FORCE_DEBUG_LEVEL |
Activate debugging logs at the specified level | XMTP_FORCE_DEBUG_LEVEL=Debug |
Subscribe only to what you need using Node’s EventEmitter interface. Events you can listen for:
Message Events
actions– an incoming actions message (interactive buttons/choices)attachment– an incoming remote attachment messagegroup-update– an incoming group update (like name change, member update, etc.)inline-attachment– an incoming inline attachment (small files sent directly in the message)intent– an incoming intent message (user's response to an actions message)leave-request– an incoming leave request from a member wanting to leave a groupmarkdown– an incoming markdown-formatted text messagemessage– all messages that are not having a custom content typemulti-attachment– an incoming message with multiple remote attachmentsreaction– an incoming reaction messageread-receipt– an incoming read receipt notificationreply– an incoming reply messagetext– an incoming text messagetransaction-reference– an incoming onchain transaction referencewallet-send-calls– an incoming wallet transaction request (batch calls)unknownMessage– a message event that does not correspond to any of the pre-implemented event types
Conversation Events
conversation– a new group or DM conversationdm– a new DM conversationgroup– a new group conversation
Lifecycle Events
start/stop– agent lifecycle eventsunhandledError– unhandled errors
Example
// Listen to specific message types
agent.on("text", async (ctx) => {
console.log(`Text message: ${ctx.message.content}`);
});
agent.on("reaction", async (ctx) => {
console.log(`Reaction: ${ctx.message.content}`);
});
agent.on("reply", async (ctx) => {
console.log(`Reply to: ${ctx.message.content.reference}`);
});
// Listen to new conversations
agent.on("dm", async (ctx) => {
await ctx.conversation.send("Welcome to our DM!");
});
agent.on("group", async (ctx) => {
await ctx.conversation.send("Hello group!");
});
// Listen to unhandled events
agent.on("unhandledError", (error) => {
console.error("Agent error", error);
});
agent.on("unknownMessage", (ctx) => {
console.error("Message type is unknown", ctx);
});
⚠️ Important: The"message"event fires for every incoming message, regardless of type. When using the"message"event, always filter message types to prevent infinite loops. Without proper filtering, your agent might respond to its own messages or react to system messages like read receipts.
Best Practice Example
import { filter } from "@xmtp/agent-sdk";
agent.on("message", async (ctx) => {
// Filter for specific message types
if (filter.isText(ctx.message)) {
await ctx.conversation.send(`Echo: ${ctx.message.content}`);
}
});Extend your agent with custom business logic using middlewares. Compose cross-cutting behavior like routing, telemetry, rate limiting, analytics, and feature flags, or plug in your own.
Middlewares can be registered with agent.use either one at a time or as an array. They are executed in the order they were added.
Middleware functions receive a ctx (context) object and a next function. Normally, a middleware calls next() to hand off control to the next one in the chain. However, a middleware can also alter the flow in the following ways:
- Use
next()to continue the chain and pass control to the next middleware - Use
returnto stop the chain and prevent events from firing - Use
throwto trigger the error-handling middleware chain
Example
import { Agent, AgentMiddleware, filter } from "@xmtp/agent-sdk";
const onlyText: AgentMiddleware = async (ctx, next) => {
if (filter.isText(ctx.message)) {
// Continue to next middleware
await next();
}
// Break middleware chain
return;
};
const agent = await Agent.createFromEnv();
agent.use(onlyText);Error middleware can be registered with agent.errors.use either one at a time or as an array. They are executed in the order they were added.
Error middleware receives the error, ctx, and a next function. Just like regular middleware, the flow in error middleware depends on how to use next:
- Use
next()to mark the error as handled and continue with the main middleware chain - Use
next(error)to forward the original (or transformed) error to the next error handler - Use
returnto end error handling and stop the middleware chain - Use
throwto raise a new error to be caught by the error chain
Example
import { Agent, AgentErrorMiddleware } from "@xmtp/agent-sdk";
const errorHandler: AgentErrorMiddleware = async (error, ctx, next) => {
if (error instanceof Error) {
// Transform the error and pass it along
await next(`Validation failed: ${error.message}`);
} else {
// Let other error handlers deal with it
await next(error);
}
};
const agent = await Agent.createFromEnv();
agent.errors.use(errorHandler);Any error not handled by custom error middleware is caught by the default error handler and published to the unhandledError topic, where it can be observed.
Example
agent.on("unhandledError", (error) => {
console.log("Caught error", error);
});Built‑in, officially supported middleware is provided by the Agent SDK.
| Middleware | Description |
|---|---|
CommandRouter |
Slash-command routing with optional help generation and a default handler |
PerformanceMonitor |
Measures message processing time and logs periodic CPU / memory health reports |
Example: CommandRouter
The CommandRouter makes it easy to handle slash commands out of the box.
import { Agent, CommandRouter } from "@xmtp/agent-sdk";
const agent = await Agent.createFromEnv();
const router = new CommandRouter()
.command("/hello", async (ctx) => {
await ctx.conversation.send("Hi there! 👋");
})
.default(async (ctx) => {
await ctx.conversation.send(`Unknown command: ${ctx.message.content}`);
});
agent.use(router.middleware());Instead of manually checking every incoming message, you can use the provided filters.
Example
import { filter } from "@xmtp/agent-sdk";
// Using filter in message handler
agent.on("text", async (ctx) => {
if (filter.isText(ctx.message)) {
await ctx.conversation.send("You sent a text message!");
}
});
// Combine multiple conditions
agent.on("text", async (ctx) => {
if (
filter.hasDefinedContent(ctx.message) &&
!filter.fromSelf(ctx.message, ctx.client) &&
filter.isText(ctx.message)
) {
await ctx.conversation.send("Valid text message received ✅");
}
});For convenience, the filter object can also be imported as f:
// You can import either name:
import { filter, f } from "@xmtp/agent-sdk";
// Both work the same way:
if (f.isText(ctx.message)) {
// Handle message...
}Available Filters:
You can find all available prebuilt filters here.
Every message event handler receives a MessageContext (ctx) with:
message– the decoded message objectconversation– the active conversation objectclient– underlying XMTP client- Helpers like
sendTextReply,sendMarkdown,sendReaction,getSenderAddress, and more
Example
agent.on("text", async (ctx) => {
await ctx.sendTextReply("Reply using helper ✨");
});The Agent class also exposes a function to get the ConversationContext, so you can directly interact with a conversation.
Example
const ctx = await agent.getConversationContext("conversationId");
await ctx?.sendMarkdown("**Hello, World!**");These functionalities let you start a conversation:
// Direct Message
const dm = await agent.createDmWithAddress("0x123");
await dm.send("Hello!");
// Group Conversation
const group = await agent.createGroupWithAddresses(["0x123", "0x456"]);
await group.addMembers(["0x789"]);
await group.send("Hello group!");The Agent SDK includes various utilities. You can for example get a testing URL or details of your Agent:
import { getTestUrl, logDetails } from "@xmtp/agent-sdk";
// Get a test URL for your agent
const testUrl = getTestUrl(agent.client);
console.log(`Test your agent at: ${testUrl}`);
// Log comprehensive details about your agent
await logDetails(agent.client);There are also utilities to simplify user management, such as signer creation or name resolutions:
import { createUser, createSigner, createNameResolver } from "@xmtp/agent-sdk";
// Create a new user with a random private key
const user = createUser();
// Create a signer from the user
const signer = createSigner(user);
// Resolve ENS names or other web3 identities using web3.bio
const resolver = createNameResolver("your-web3bio-api-key");
const address = await resolver("vitalik.eth");
console.log(`Resolved address: ${address}`);The Agent SDK supports sending encrypted remote attachments. Files are encrypted locally, uploaded to your storage provider of choice, and then sent as a remote attachment message containing the URL and decryption keys.
Example
import { type AttachmentUploadCallback } from "@xmtp/agent-sdk";
agent.on("text", async (ctx) => {
if (ctx.message.content === "/send-file") {
// Create a File object (in Node.js, you can use the File class from buffer or file-system)
const file = new File(["Hello, World!"], "hello.txt", {
type: "text/plain",
});
// Upload callback - implement your own storage solution
const uploadCallback: AttachmentUploadCallback = async (attachment) => {
// Upload "attachment.content.payload" to your storage
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
pinataGateway: process.env.PINATA_GATEWAY,
});
const mimeType = "application/octet-stream";
const encryptedBlob = new Blob(
[Buffer.from(attachment.content.payload)],
{
type: mimeType,
},
);
const encryptedFile = new File([encryptedBlob], attachment.filename, {
type: mimeType,
});
const upload = await pinata.upload.public.file(encryptedFile);
// Return the public URL where the file can be downloaded
return pinata.gateways.public.convert(`${upload.cid}`);
};
// Send the encrypted remote attachment
await ctx.sendRemoteAttachment(file, uploadCallback);
}
});Other agents can then download and decrypt the attachment using the "attachment" topic:
import { downloadRemoteAttachment } from "@xmtp/agent-sdk";
agent.on("attachment", async (ctx) => {
const receivedAttachment = await downloadRemoteAttachment(
ctx.message.content,
agent,
);
console.log(`Received attachment: ${receivedAttachment.filename}`);
});Pass codecs when creating your agent to extend supported content:
const agent = await Agent.create(signer, {
env: "dev",
dbPath: null,
codecs: [new MyContentType()],
});LibXMTP is a shared library encapsulating the core functionality of the XMTP messaging protocol, such as cryptography, networking, and language bindings.
The LibXMTP version used in the Agent SDK can be accessed with the libxmtpVersion property of an agent instance.
console.log(agent.libxmtpVersion);We’d love your feedback: open an issue or discussion. PRs welcome for docs, examples, and core improvements.
Build something delightful. Then tell us what you wish was easier.
Happy hacking 💫