Connectors
Discord
Talk to an agent with a slash command in Discord.
Quickstart
Create a Discord application
In the Discord Developer Portal, create an application. Copy its application ID and public key, and create a bot token.
Register the command
Register an /ask command with a prompt option:
curl -X PUT "https://discord.com/api/v10/applications/$DISCORD_APPLICATION_ID/commands" \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"name":"ask","description":"Ask the agent","options":[{"type":3,"name":"prompt","description":"What to ask","required":true}]}]'
Install the application to your server with the applications.commands scope.
Add the route
npm add @rivet-dev/pi rivetkit hono discord-interactions
import { pi } from "@rivet-dev/pi";
import { Hono } from "hono";
import { setup } from "rivetkit";
import { type DiscordCommand, discord, postReply } from "./discord";
const agent = pi({
model: "anthropic/claude-opus-5-5",
actions: {
receive: async (c, command: DiscordCommand) => {
await c.schedule.after(0, "reply", command);
},
reply: async (c, command: DiscordCommand): Promise<void> => {
const self = c.client<typeof registry>().agent.getForId(c.actorId);
await self.prompt(command.prompt);
await postReply(command, await self.getLastAssistantText());
},
},
});
export const registry = setup({ use: { agent } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.route("/discord", discord);
export default app;
import { InteractionResponseType, InteractionType, verifyKey } from "discord-interactions";
import { Hono } from "hono";
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
export type DiscordCommand = { applicationId: string; token: string; prompt: string };
const client = createClient<typeof registry>();
export const discord = new Hono();
discord.post("/interactions", async (c) => {
const body = await c.req.text();
const signature = c.req.header("x-signature-ed25519") ?? "";
const timestamp = c.req.header("x-signature-timestamp") ?? "";
if (!(await verifyKey(body, signature, timestamp, process.env.DISCORD_PUBLIC_KEY ?? ""))) {
return c.text("invalid signature", 401);
}
const interaction = JSON.parse(body);
if (interaction.type === InteractionType.PING) return c.json({ type: InteractionResponseType.PONG });
if (interaction.type === InteractionType.APPLICATION_COMMAND && interaction.data.name === "ask") {
const prompt = interaction.data.options?.find((option: { name: string }) => option.name === "prompt")?.value;
const agent = client.agent.getOrCreate(["discord", interaction.channel_id ?? interaction.channel?.id]);
await agent.receive({ applicationId: interaction.application_id, token: interaction.token, prompt: String(prompt ?? "") });
return c.json({ type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE });
}
return c.text("unhandled interaction", 400);
});
export async function postReply(command: DiscordCommand, reply: string | undefined) {
const url = `https://discord.com/api/v10/webhooks/${command.applicationId}/${command.token}/messages/@original`;
await fetch(url, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content: (reply || "I finished without a reply.").slice(0, 2000) }),
});
}
Run the server with any runtime that serves a fetch handler, for example npx srvx --import tsx server.ts. See HTTP Server.
Set the environment
DISCORD_PUBLIC_KEY=...
ANTHROPIC_API_KEY=sk-ant-...
Set the interactions endpoint
Under General Information, set the interactions endpoint URL to https://<your-worker>/discord/interactions. Discord sends a ping, which the route answers. Run /ask in a channel.
How it works
- The route verifies Discord’s Ed25519 signature against the raw body with the application’s public key.
- Each Discord channel gets its own agent. Every
/askin the channel continues the same session. - Discord expects a response within three seconds and an agent run takes longer, so the route hands the command to the agent’s
receiveaction and defers the reply.receiveschedulesreply, which prompts the agent and edits the deferred message. Interaction tokens stay valid for 15 minutes, and a message holds up to 2,000 characters. The schedule is saved with the agent, so the reply still runs if the request that delivered the message goes away.
Configure
| Variable | Purpose |
|---|---|
DISCORD_PUBLIC_KEY | Required. Verifies that requests come from Discord. |
DISCORD_APPLICATION_ID | Registers the command. Not needed by the server. |
DISCORD_BOT_TOKEN | Registers the command. Not needed by the server. |
ANTHROPIC_API_KEY | Required. The agent’s model key. See API Keys. |
Supported events
| Discord interaction | Webhook path | What the route does |
|---|---|---|
PING | /discord/interactions | Answers Discord’s endpoint check. |
Application command /ask | /discord/interactions | Hands the command to the channel’s agent and defers. The agent edits the reply. |