eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
108 lines (74 loc) • 5.47 kB
text/mdx
---
title: "Discord"
description: "Reach your agent from Discord HTTP Interactions, including slash commands, components, and modals."
type: integration
---
The Discord channel wires your agent into Discord's HTTP Interactions, including slash and application commands, message components, and modal submissions. Discord enforces a three-second ACK deadline, so the channel acknowledges the command right away and runs the eve work in the background. Credentials and inbound request verification run through [Vercel Connect](../guides/auth-and-route-protection), so Discord's bot token stays out of your environment and Connect verifies Discord's signature before forwarding an interaction to eve. See [Channels](./overview) for the contract this builds on.
## Set up Discord
Run the guided setup from your eve project:
```bash
pnpm eve add channel/discord
```
The setup guides you through creating a Discord application and bot, then:
- signs you in to Vercel and creates or links a project when needed;
- validates the bot token;
- creates a Vercel Connect client and attaches `/eve/v1/discord` as its trigger destination;
- registers an application command with a required `message` option;
- configures the Discord application's Interactions Endpoint URL;
- scaffolds the channel with Connect-managed credentials; and
- prints the bot installation URL.
The command defaults to `/ask` with the description **Ask the eve agent**. You can edit both during setup. Discord global commands can take up to an hour to appear after registration.
The generated channel looks like this:
```ts title="agent/channels/discord.ts"
import { connectDiscordCredentials } from "@vercel/connect/eve";
import { discordChannel } from "eve/channels/discord";
export default discordChannel({
credentials: connectDiscordCredentials("discord/my-agent"),
});
```
The route is `POST /eve/v1/discord` by default. Connect resolves the application ID and bot token lazily and verifies forwarded interactions with same-project Vercel OIDC.
### Configure Discord manually
If you do not use the guided Connect setup, register a command with Discord's API or the Developer Portal. A string option named `message` lines up with eve's default prompt extraction:
```bash
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 eve agent","type":1,
"options":[{"name":"message","description":"What should the agent do?","type":3,"required":true}]}]'
```
Then configure the public `https://…/eve/v1/discord` route as the application's Interactions Endpoint URL. Pass `credentials: { applicationId, botToken, publicKey }` to `discordChannel`, or set the corresponding `DISCORD_APPLICATION_ID`, `DISCORD_BOT_TOKEN`, and `DISCORD_PUBLIC_KEY` environment variables.
## How the channel handles messages
### Dispatch
`onCommand(ctx, interaction)` decides whether to dispatch and under what `auth`. Return `{ auth }` to proceed, `null` to drop the interaction, or `title` alongside `auth` to set the title when the dispatch starts a run. By default, auth comes from the invoking user.
The model-visible `<discord_context>` includes `user_id` and the receiving application's `application_id`.
Event handlers receive `(eventData, channel, ctx)`, with Discord platform handles on `channel.discord`:
```ts
import { discordChannel } from "eve/channels/discord";
export default discordChannel({
onCommand: (ctx, interaction) => ({
auth: {
principalId: interaction.user.id,
principalType: "user",
authenticator: "discord",
attributes: { channel_id: interaction.channelId, guild_id: interaction.guildId ?? "" },
},
}),
events: {
"message.completed"(eventData, channel, ctx) {
if (eventData.finishReason === "tool-calls") return;
if (eventData.message) channel.discord.post(eventData.message);
},
},
});
```
### Delivery
The default `message.completed` handler edits the deferred response for the first reply and sends followups after that. If the interaction token is rejected, it falls back to a bot-authenticated channel message. Long text is split to Discord's 2000-char limit, and generated messages default to `allowed_mentions: { parse: [] }`.
Typing fires on `turn.started` and `actions.requested`, but only when a bot token is present. In custom hooks, call `channel.discord.startTyping()` yourself.
### Human-in-the-loop (HITL)
HITL renders as Discord components. Confirmations and options become buttons, `display: "select"` becomes a string select, and freeform input becomes a button that opens a modal. When the user responds, the parked session (paused awaiting input) resumes.
### Proactive sessions
Start a session without an inbound interaction through `to(discord, target).send(message, { auth })` from a schedule `run` handler, or `ctx.to(discord, target).send(message, { auth })` from another channel. The proactive target shape is `{ channelId, conversationId?, initialMessage? }`. Either path needs a bot token, supplied by Connect or `DISCORD_BOT_TOKEN`.
### Attachments
Inbound file attachments are not supported on this channel today.
## What to read next
- [Channels overview](./overview): the channel contract and every built-in channel
- [Auth & route protection](../guides/auth-and-route-protection): authenticating inbound traffic