XMTP Agent SDK
Build event‑driven, middleware‑powered messaging agents on the XMTP network. 🚀
Documentation
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.
Installation
Choose your package manager:
npm install @xmtp/agent-sdk
pnpm add @xmtp/agent-sdk
yarn add @xmtp/agent-sdk
Quick Start
import { Agent } from "@xmtp/agent-sdk";
import { getTestUrl } from "@xmtp/agent-sdk/debug";
import { createUser, createSigner } from "@xmtp/agent-sdk/user";
const user = createUser();
const signer = createSigner(user);
const agent = await Agent.create(signer, {
env: "dev",
dbPath: null,
});
agent.on("text", async (ctx) => {
await ctx.sendText("Hello from my XMTP Agent! 👋");
});
agent.on("start", (ctx) => {
console.log(`We are online: ${getTestUrl(ctx.client)}`);
});
await agent.start();
Environment Variables
The XMTP Agent SDK allows you to use environment variables (process.env
) for easier configuration without modifying code. Simply set the following variables and call Agent.createFromEnv()
:
Available Variables:
Using the environment variables, you can setup your agent in just a few lines of code:
process.loadEnvFile(".env");
const agent = await Agent.createFromEnv();
Agents can also recognize the following environment variables:
Core Concepts
1. Event‑Driven Architecture
Subscribe only to what you need using Node’s EventEmitter
interface. Events you can listen for:
Message Events
attachment
– a new incoming remote attachment message
message
– all incoming messages (fires for every message regardless of type)
reaction
– a new incoming reaction message
reply
– a new incoming reply message
text
– a new incoming text message
unknownMessage
– a message that doesn't match any specific type
Conversation Events
conversation
– a new group or DM conversation
dm
– a new DM conversation
group
– a new group conversation
Lifecycle Events
start
/ stop
– agent lifecycle events
unhandledError
– unhandled errors
Example
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}`);
});
agent.on("dm", async (ctx) => {
await ctx.conversation.send("Welcome to our DM!");
});
agent.on("group", async (ctx) => {
await ctx.conversation.send("Hello group!");
});
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) => {
if (filter.isText(ctx.message)) {
await ctx.conversation.send(`Echo: ${ctx.message.content}`);
}
});
2. Middleware Support
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.
Standard Middleware
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
return
to stop the chain and prevent events from firing
- Use
throw
to 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)) {
await next();
}
return;
};
const agent = await Agent.createFromEnv();
agent.use(onlyText);
Error-Handling Middleware
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
return
to end error handling and stop the middleware chain
- Use
throw
to 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) {
await next(`Validation failed: ${error.message}`);
} else {
await next(error);
}
};
const agent = await Agent.createFromEnv();
agent.errors.use(errorHandler);
Default Error Handler
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);
});
3. Built‑in Filters
Instead of manually checking every incoming message, you can use the provided filters.
Example
import { filter } from "@xmtp/agent-sdk";
agent.on("text", async (ctx) => {
if (filter.isText(ctx.message)) {
await ctx.conversation.send("You sent a text message!");
}
});
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
:
import { filter, f } from "@xmtp/agent-sdk";
if (f.isText(ctx.message)) {
}
Available Filters:
You can find all available prebuilt filters here.
4. Rich Context
Every message event handler receives a MessageContext
with:
message
– the decoded message object
conversation
– the active conversation object
client
– underlying XMTP client
- Helpers like
sendTextReply()
, sendReaction()
, getSenderAddress
, and more
Example
agent.on("text", async (ctx) => {
await ctx.sendTextReply("Reply using helper ✨");
});
5. Starting Conversations
These functionalities let you start a conversation:
const dm = await agent.createDmWithAddress("0x123");
await dm.send("Hello!");
const group = await agent.createGroupWithAddresses(["0x123", "0x456"]);
await group.addMembers(["0x789"]);
await group.send("Hello group!");
Adding Custom Content Types
Pass codecs
when creating your agent to extend supported content:
const agent = await Agent.create(signer, {
env: "dev",
dbPath: null,
codecs: [new MyContentType()],
});
Debugging
Contributing / Feedback
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 💫