Instruction file imported from beora-hq/Beora-App (
.cursor/rules/xmtp-framework.mdc). Copyright stays with the author.
XMTP Framework Rules
This project implements a modular, plugin-based XMTP messaging framework built on top of @xmtp/agent-sdk. The framework provides a clean abstraction for building XMTP-powered messaging agents with extensible plugins, middleware, and event handling.
Architecture Overview
src/xmtp/
├── core/ # Core framework components
│ ├── base.ts # XMTPBase - Main orchestrator
│ ├── client.ts # XMTPClient - Agent creation
│ ├── serviceRegistry.ts # Plugin management
│ ├── types.ts # TypeScript types & interfaces
│ ├── utils.ts # Utility functions
│ └── index.ts # Barrel exports
├── middleware/ # Middleware components
│ ├── commandRouter.middleware.ts # Command routing
│ ├── logging.middleware.ts # Message logging
│ └── index.ts
└── plugins/ # Service plugins
├── *.plugin.ts # Individual plugins
└── index.ts
Core Components
XMTPBase (src/xmtp/core/base.ts)
The main orchestrator class that manages the XMTP agent lifecycle:
const xmtpBase = new XMTPBase();
// Register plugins using fluent API
xmtpBase.use(new BroadcastPlugin());
xmtpBase.use(new TestPlugin());
// Initialize and start
await xmtpBase.init();
await xmtpBase.start();
Key Methods:
use(plugin)- Register a plugin (chainable)init()- Initialize XMTP client, plugins, and middlewarestart()- Start the agent and begin listeningstop()- Graceful shutdowngetAgent()- Access the XMTP Agent instancegetClient()- Access the XMTP Client instance
XMTPClient (src/xmtp/core/client.ts)
Handles XMTP agent creation with custom codecs:
const xmtpClient = new XMTPClient();
const agent = await xmtpClient.createAgent();
Configuration: Uses environment variables via config.xmtp for:
env- XMTP environment ("dev" | "production")volumeMountPath- Database storage path
ServiceRegistry (src/xmtp/core/serviceRegistry.ts)
Manages plugin lifecycle and priority-based execution:
// Plugins are sorted by priority (lower = higher priority)
const sorted = registry.getSortedPlugins();
Plugin System
Creating a Plugin
All plugins extend XMTPServicePlugin abstract class:
import {
ActionHandler,
CommandHandler,
EventResult,
PluginMetadata,
XMTPServicePlugin
} from "@/xmtp/core/index.js";
import { ActionBuilder } from "@/xmtp/content-types/inline-actions/index.js";
export class MyPlugin extends XMTPServicePlugin {
metadata: PluginMetadata = {
name: "MyPlugin",
version: "1.0.0",
priority: 50, // 1-100 (1 = highest priority)
description: "Description of my plugin",
};
// Lifecycle hooks
async onInit(): Promise<void> {
// Called when plugin initializes
// this.client and this.agent are available here
}
async onShutdown(): Promise<void> {
// Called during graceful shutdown
}
// Register slash commands
getCommands(): Map<string, CommandHandler> {
const commands = new Map<string, CommandHandler>();
commands.set("/mycommand", async (ctx) => {
await ctx.conversation.send("Response!");
});
commands.set("/menu", async (ctx) => {
// Show inline action buttons
await ActionBuilder.create("my-menu", "Choose an option:")
.add("my-action", "Click Me")
.add("my-other-action", "Other Option")
.send(ctx);
});
return commands;
}
// Register inline actions (button click handlers)
getActions(): Map<string, ActionHandler> {
const actions = new Map<string, ActionHandler>();
actions.set("my-action", async (ctx) => {
await ctx.sendText("You clicked the button!");
});
actions.set("my-other-action", async (ctx) => {
await ctx.sendText("Other action executed!");
});
return actions;
}
// Event handlers - return true to stop propagation
async onText(ctx: MessageContext<string>): Promise<EventResult> {
// Handle text messages
return false; // Let other plugins process
}
async onMessage(ctx: any): Promise<EventResult> {
// Handle non-text messages
return false;
}
async onDm(ctx: any): Promise<EventResult> {
// Handle direct messages
return false;
}
async onGroup(ctx: any): Promise<EventResult> {
// Handle group messages
return false;
}
async onGroupUpdate(ctx: any): Promise<EventResult> {
// Handle group updates (members added/removed, etc.)
return false;
}
async onReaction(ctx: any): Promise<EventResult> {
// Handle reactions
return false;
}
}
Plugin Metadata
interface PluginMetadata {
name: string; // Unique plugin identifier
version?: string; // Semantic version
priority: number; // 1-100 (1 = highest priority)
description?: string; // Human-readable description
}
Event Result Pattern
Event handlers return EventResult (boolean | void):
return true- Message handled, stop propagation to other pluginsreturn falseorreturn- Message not handled, continue to next plugin
async onText(ctx: MessageContext<string>): Promise<EventResult> {
const content = ctx.message.content;
// Only handle specific patterns
if (!content.startsWith("myprefix:")) {
return false; // Not for this plugin
}
// Process the message
await ctx.conversation.send("Handled!");
return true; // Stop propagation
}
Accessing Client & Agent
Plugins have access to the XMTP client and agent via protected properties:
class MyPlugin extends XMTPServicePlugin {
async someMethod() {
// Access XMTP Client for creating conversations
const conversation = await this.client.conversations.newDm(inboxId);
await conversation.send("Hello!");
// Access Agent for agent-specific operations
const address = this.agent.address;
}
}
Middleware System
Creating Middleware
Middleware uses the AgentMiddleware type from @xmtp/agent-sdk:
import { AgentMiddleware } from "@xmtp/agent-sdk";
export const myMiddleware: AgentMiddleware = async (ctx, next) => {
// Before message processing
console.log("Message received:", ctx.message.content);
await next(); // Continue to next middleware/handler
// After message processing
console.log("Processing complete");
};
Built-in Middleware
-
Logging Middleware (
logging.middleware.ts)- Logs all incoming messages with sender, content type, and IDs
-
Inline Actions Middleware (
inlineActionsMiddleware)- Handles intent messages (button clicks)
- Routes to registered action handlers
- Actions from plugins are automatically registered via
getActions()
-
Command Router (
commandRouter.middleware.ts)- Routes slash commands to registered handlers
- Commands from plugins are automatically registered
Adding Custom Middleware
Middleware is added in XMTPBase.setupMiddleware():
private setupMiddleware(): void {
this.agent!.use(loggingMiddleware);
this.agent!.use(inlineActionsMiddleware);
this.agent!.use(myCustomMiddleware);
// Command router is added last
}
Inline Actions System
Plugins can register interactive button handlers via getActions():
Registering Actions
getActions(): Map<string, ActionHandler> {
const actions = new Map<string, ActionHandler>();
actions.set("confirm-delete", async (ctx) => {
await ctx.sendText("Item deleted!");
});
actions.set("cancel", async (ctx) => {
await ctx.sendText("Cancelled");
});
return actions;
}
Sending Action Buttons
Use ActionBuilder to send interactive buttons:
import { ActionBuilder } from "@/xmtp/content-types/inline-actions/index.js";
// In a command or event handler
await ActionBuilder.create("delete-confirm", "Delete this item?")
.add("confirm-delete", "✅ Yes", "danger")
.add("cancel", "❌ No")
.send(ctx);
Action Handler Type
type ActionHandler = (ctx: MessageContext<unknown>) => Promise<void>;
Helper Functions
See @.cursor/rules/inline-actions.mdc for complete documentation on:
sendConfirmation()- Quick Yes/No dialogssendSelection()- Dynamic option menusAppConfig- Configuration-based menu systems
Command System
Registering Commands
Commands are registered through plugin's getCommands() method:
getCommands(): Map<string, CommandHandler> {
const commands = new Map<string, CommandHandler>();
commands.set("/help", async (ctx) => {
await ctx.conversation.send("Available commands: /help, /start");
});
commands.set("/echo", async (ctx) => {
const content = ctx.message.content as string;
const message = content.replace(/^\/echo\s*/i, "").trim();
await ctx.conversation.send(`Echo: ${message}`);
});
return commands;
}
Command Handler Type
type CommandHandler = (ctx: MessageContext<string>) => Promise<void>;
Built-in Commands
The command router includes default commands:
/help- Display help information/start- Start/welcome message
Event Dispatch Flow
- Message arrives → Middleware pipeline executes
- Event type determined →
dm,group,text,reaction,message,group-update - Plugins iterated → Sorted by priority (ascending)
- Handler called → If handler returns
true, propagation stops
Message → Middleware → Event Router → Plugin1 → Plugin2 → ... → PluginN
↓
(if returns true)
↓
STOP PROPAGATION
Utility Functions
sendReaction (src/xmtp/core/utils.ts)
Send a reaction to a message:
import { sendReaction } from "@/xmtp/core/utils.js";
// In event handler
await sendReaction(ctx, "👍"); // Default is "👀"
Configuration Requirements
Environment Variables
XMTP_ENV=dev # or "production"
XMTP_WALLET_KEY=<private-key> # Wallet private key for agent
XMTP_DB_ENCRYPTION_KEY= # Database Encryption Key
XMTP_VOLUME_MOUNT_PATH=./data # Database storage path
Required Dependencies
{
"@xmtp/agent-sdk": "^x.x.x",
"@xmtp/content-type-reaction": "^x.x.x"
}
Add other dependencies and inline actions base on need
Best Practices
Plugin Design
- Single Responsibility - Each plugin handles one feature
- Priority Planning - Lower numbers = higher priority
- Graceful Returns - Return
falseto allow other plugins to process - Error Handling - Wrap operations in try/catch
Event Handling
- Pattern Matching - Check message patterns before processing
- Early Returns - Return
falseimmediately if not relevant - Stop Propagation - Return
trueonly when fully handled
Middleware
- Always call
next()- Unless intentionally stopping the chain - Lightweight operations - Middleware runs on every message
- Order matters - Add logging first, command router last
Example: Creating a New Plugin
// src/xmtp/plugins/greeting.plugin.ts
import { MessageContext } from "@xmtp/agent-sdk";
import { logger } from "@/utils/logger.js";
import {
CommandHandler,
EventResult,
PluginMetadata,
XMTPServicePlugin,
} from "@/xmtp/core/index.js";
export class GreetingPlugin extends XMTPServicePlugin {
metadata: PluginMetadata = {
name: "GreetingPlugin",
version: "1.0.0",
priority: 30,
description: "Handles greeting messages and commands",
};
async onInit(): Promise<void> {
logger.info("✅ GreetingPlugin initialized");
}
async onShutdown(): Promise<void> {
logger.info("🛑 GreetingPlugin shutting down");
}
getCommands(): Map<string, CommandHandler> {
const commands = new Map<string, CommandHandler>();
commands.set("/hello", async (ctx) => {
await ctx.conversation.send("Hello! How can I help you today?");
});
commands.set("/bye", async (ctx) => {
await ctx.conversation.send("Goodbye! Have a great day!");
});
return commands;
}
async onText(ctx: MessageContext<string>): Promise<EventResult> {
const content = ctx.message.content.toLowerCase();
if (content.includes("hello") || content.includes("hi")) {
await ctx.conversation.send("Hey there! 👋");
return true;
}
return false;
}
}
Registering the Plugin
// src/index.ts
import { XMTPBase } from "@/xmtp/core/index.js";
import { GreetingPlugin } from "@/xmtp/plugins/greeting.plugin.js";
const xmtpBase = new XMTPBase();
xmtpBase.use(new GreetingPlugin());
await xmtpBase.init();
await xmtpBase.start();
Exporting the Plugin
// src/xmtp/plugins/index.ts
export { GreetingPlugin } from "./greeting.plugin.js";
File Naming Conventions
- Plugins:
*.plugin.ts - Middleware:
*.middleware.ts - Index files:
index.tsfor barrel exports - Use
.jsextension in imports (for ESM compatibility)
TypeScript Configuration
The framework uses path aliases:
@/→src/
Example: import { logger } from "@/utils/logger.js";