Discord Setup
Configure an Ed25519-signed Discord endpoint, install the bot, and choose a truthful server, thread, or DM routing boundary.
- Audience
- Operators running durable agent work
- Focus
- Bridges guidance shaped for scanability, day-two clarity, and operator context.
This guide connects a Discord application to one AGH workspace. The in-tree provider is an HTTP integration: it handles Discord interactions and Application Webhook Events, then uses Discord REST for outbound messages. It does not open a Discord Gateway connection.
How the Discord bridge behaves
| Surface | AGH behavior |
|---|---|
| Inbound transport | Receives Ed25519-signed interaction payloads and Application Webhook Events over public HTTPS. |
| Routed events | Maps MESSAGE_CREATE, reaction add/remove, application commands, and message components into typed bridge events. |
| Event boundary | Does not ingest ordinary MESSAGE_UPDATE; that event requires a Discord Gateway lifecycle outside this provider. |
| Routing | Maps guild channels, threads, and DMs into distinct dimensions; the instance policy decides which dimensions become the session and reply target. |
| Outbound delivery | Creates, edits, and deletes Discord messages through REST. |
| Long responses | Splits terminal text at 2,000 Unicode code points with ordered, numbered continuations. |
| Tool progress | Defaults to new + accumulate with one edited progress message, typing, and phase reactions. Final text uses a separate acknowledgement anchor. |
This differs from Discord bot guides built around Gateway intents. AGH does not need a long-lived Gateway socket or privileged message-content intent for its HTTP webhook path.
Before you start
| Value | Required | Where it comes from |
|---|---|---|
| Application ID | yes | Discord Developer Portal General Information. |
| Ed25519 public key | yes | Discord Developer Portal General Information; exactly 64 hexadecimal characters. |
| Bot token | yes | Discord Developer Portal Bot page. |
| Public callback URL | yes | Stable public HTTPS route to the Discord provider listener. |
| Local listener address | yes | provider_config.webhook.listen_addr or AGH_BRIDGE_DISCORD_LISTEN_ADDR. |
| AGH workspace ID | yes | Workspace whose default agent receives accepted Discord events. |
| Server permissions | yes | Permission to install an app in the test server and configure its webhook/event settings. |
Keep the public key and bot token distinct. The public key verifies inbound requests; the bot token authorizes outbound REST calls.
Before configuring Discord, prove that the locally built provider is installed and visible in the daemon catalog:
PROVIDER=discord
agh extension status "$PROVIDER" -o json
curl -sS http://127.0.0.1:2123/api/bridges/providers | \
jq --arg provider "$PROVIDER" '.providers[] | select(.extension_name == $provider)'Continue only when the extension is enabled and the catalog row is not disabled or unhealthy. If either command has no provider, follow Install an in-tree provider first.
Step 1: Create the Discord application and bot
- Open the Discord Developer Portal.
- Select New Application and choose a recognizable name.
- On General Information, copy the Application ID and Public Key.
- Open Bot, create the bot user if needed, and reset/copy the bot token.
- Store the token in a secret manager. Discord shows it only at controlled reset points.
Do not enable privileged Gateway intents for this bridge unless another component of the same app uses a Gateway connection. They are not part of AGH's inbound contract.
Step 2: Map the public and local endpoints
Discord → https://bridge.example.com/discord/support
→ reverse proxy or tunnel
→ http://127.0.0.1:18083/discord/supportThe proxy must preserve the raw body and these headers:
X-Signature-TimestampX-Signature-Ed25519
Discord signs the timestamp plus exact body bytes. Decompression, re-encoding, or JSON normalization before the provider verifies the request breaks authentication.
Step 3: Run guided setup
Interactive setup creates the bridge disabled, validates the application identity values, binds the bot token/public key, and returns an install URL:
agh bridge setup discordThe interactive wizard creates a server-wide route with include_group=true. Every event in the
same parent channel shares one AGH session, and replies target that parent channel. It accepts
ordinary guild-channel, Group DM, and server-thread events, but does not retain the thread in the
reply target and rejects direct DMs. Choose another policy explicitly when that is not the intended
boundary.
The default invite requests:
- scopes:
botandapplications.commands; - permission bits:
68672, covering view channel, send messages, add reactions, and read message history.
The wizard does not register application commands or choose server/channel access for you. It produces the OAuth install boundary; the Discord application remains operator-owned.
Headless setup
agh bridge setup discord --json <<'JSON'
{
"scope": "workspace",
"workspace_id": "ws_8f33a913d23c4fd1",
"routing_policy": {
"include_peer": false,
"include_thread": false,
"include_group": true
},
"webhook_public_url": "https://bridge.example.com/discord/support",
"webhook_path": "/discord/support",
"application_id": "111122223333444455",
"bot_token": "replace-with-the-discord-bot-token",
"public_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"invite_scopes": ["bot", "applications.commands"],
"invite_permissions": 68672
}
JSONUnknown fields and trailing JSON values fail before writes. The result contains masked bindings, the invite URL, the verification command, and the next lifecycle action.
Copy the returned bridge ID:
BRIDGE_ID=brg_123Choose one routing shape
Route dimensions are conjunctive: every selected dimension must exist on an inbound event.
| Intended traffic | routing_policy | Result |
|---|---|---|
| Guild channels and their threads | include_group=true | One session and reply target per parent channel; this is the wizard default. |
| Server threads only | include_group=true, include_thread=true | One session and reply target per thread; ordinary channel events fail routing. |
| Direct messages only | include_peer=true | One session and reply target per Discord DM. |
Do not combine peer and group. Discord emits one or the other, so a peer-plus-group policy rejects both DMs and guild events. If one application needs independently routed server and DM traffic, use separate bridge instances and callback paths, backed by separate Discord application endpoint configurations or an operator-owned dispatcher. Leaving all route dimensions false is not a dynamic reply path: the daemon cannot construct a Discord peer or group delivery target from that key.
Step 4: Configure the Discord endpoints
In the Developer Portal for the same application:
- Set Interactions Endpoint URL to the exact
webhook_public_urlreturned by setup. - Save the endpoint. Discord sends a signed PING; AGH validates Ed25519 and returns PONG.
- Configure Application Webhook Events you intend to route, using the same endpoint.
- Select message-create and reaction add/remove event families only when your app needs them.
The provider can parse application commands and message components delivered to the interaction endpoint, but command definitions and component-producing messages are separate Discord application configuration. AGH does not fabricate a command catalog during setup.
Step 5: Install the bot
- Open the
invite_urlreturned by setup. - Select the test server.
- Review the requested permissions.
- Authorize the application.
- Restrict the bot to the intended channels or roles in Discord server settings when needed.
If you change invite scopes or permission bits, rerun setup for the existing bridge or build a new OAuth URL from the same application ID. Do not install a second application with a different public key and reuse the first bridge's secrets.
Step 6: Verify the disabled bridge
agh bridge verify "$BRIDGE_ID" --jsonExpected evidence:
provider.identitypasses when the bot token resolves to the configured application/bot ID;webhook.public_keypasses when the Ed25519 public-key binding exists;- webhook reachability is
skippedwhile disabled.
An application-ID mismatch means the bot token and portal application do not belong together. Fix the values rather than removing the configured ID.
Step 7: Control direct-message access
Direct DMs are evaluated by dm_policy before routing. An omitted or empty value normalizes to
open, which admits every Discord DM sender whose request passes provider authentication. Any
admitted direct DM still fails the wizard's group-only route because it emits peer_id, not
group_id; a direct-DM instance must use include_peer=true.
agh bridge create and agh bridge update do not currently expose --dm-policy. Use the Web editor
or HTTP API procedure in Control direct-message access, then keep the bridge disabled
until agh bridge get "$BRIDGE_ID" -o json reports the intended policy and lists.
Provider config accepts stable Discord user IDs and normalized username fallbacks:
{
"dm": {
"allow_user_ids": ["123456789012345678"],
"allow_usernames": ["alice"],
"paired_user_ids": [],
"paired_usernames": []
}
}pairing reads pre-populated paired_user_ids and paired_usernames, then falls back to the
allowlist; it does not issue pairing codes or start an interactive approval flow. Prefer numeric IDs.
A display name, nickname, and username are not interchangeable identities.
The policy applies only to direct DMs. Group DMs emit group_id, are not evaluated by dm_policy,
and follow the group route. Server access remains governed by application installation, channel or
role permissions, event subscriptions, and bridge routing.
Step 8: Enable and prove inbound routing
agh bridge enable "$BRIDGE_ID"
agh bridge verify "$BRIDGE_ID" --json
agh bridge get "$BRIDGE_ID" -o jsonThen trigger a supported event:
- send a server message where the configured Application Webhook Events subscription applies;
- invoke a registered application command;
- activate a message component the application owns; or
- add/remove a reaction on a routed message.
Inspect the resulting route:
agh bridge routes "$BRIDGE_ID" -o jsonWith the wizard's default policy, the route contains only the parent channel as group_id; the
reply also targets that parent channel even when the inbound event came from a thread. A
thread-preserving instance contains both the parent group_id and thread-channel thread_id. A
direct-DM instance uses the Discord DM channel ID as peer_id, not the user ID.
Step 9: Prove outbound delivery
For the wizard's group-only policy, use the parent channel ID returned by the route:
agh bridge send-test "$BRIDGE_ID" \
--message "AGH Discord connection check" \
--group-id "123456789012345678" \
--mode reply \
--jsonFor an instance configured with group plus thread, pass both IDs from its route:
agh bridge send-test "$BRIDGE_ID" \
--message "AGH Discord thread check" \
--group-id "123456789012345678" \
--thread-id "234567890123456789" \
--mode reply \
--jsonConfirm the message appears in the policy-selected channel or thread and the result contains the Discord remote message ID.
Delivery and progress behavior
Discord measures 2,000 Unicode code points after delivery text is prepared. Terminal answers that exceed the limit become numbered continuations; all parts stay within the wire cap and remain in the same destination.
Discord defaults to:
{
"progress": {
"tool_progress": "new",
"grouping": "accumulate",
"typing": true,
"reactions": true
}
}The provider has a start-typing endpoint but no explicit clear request. It stops issuing typing requests when content arrives. Progress reactions apply to the progress message, not the final answer.
Disable optional affordances without disabling text delivery:
agh bridge update "$BRIDGE_ID" \
--delivery-progress off \
--delivery-progress-typing=false \
--delivery-progress-reactions=falseConfiguration reference
| Field | Default | Purpose |
|---|---|---|
application_id | none | Expected identity for the authenticated bot token and invite generation. |
webhook.public_url | none | Complete public Discord endpoint and enabled reachability target. |
webhook.listen_addr | AGH_BRIDGE_DISCORD_LISTEN_ADDR | Local listener shared by Discord instances. |
webhook.path | /discord/<bridge-id> | Local callback path; each instance needs a unique path. |
invite.scopes | guided setup values | OAuth scopes used to construct the operator handoff URL. |
invite.permissions | 68672 in guided setup | Decimal Discord permission bitset placed in the invite URL. |
batching.delay_ms | 0 | Enables inbound batching when positive. |
batching.split_delay_ms | delay_ms | Delay after the configured split threshold. |
batching.split_threshold | 0 | Provider batching split threshold. |
dm.allow_user_ids / paired_user_ids | empty | Discord IDs admitted by allowlist or pairing. |
dm.allow_usernames / paired_usernames | empty | Normalized username fallbacks; IDs are safer. |
bot_token and public_key are required write-only bindings. The Discord REST destination is an
operator-owned process setting and cannot be redirected by bridge instance configuration.
Common operations
Rotate the bot token
agh bridge disable "$BRIDGE_ID"
agh bridge setup discord --instance "$BRIDGE_ID"
agh bridge enable "$BRIDGE_ID"
agh bridge verify "$BRIDGE_ID" --jsonUse the same application ID and public key unless the Discord application itself changed. A bot-token reset invalidates the old token immediately.
Change the endpoint or public key
Disable the bridge first. Update the complete provider configuration or rerun guided setup, update the Discord portal endpoint, then wait for the signed PING validation before reenabling. Replacing the public key means the portal application identity changed; review the bot token and application ID as one credential set.
Troubleshooting
| Symptom | What to check |
|---|---|
| Discord rejects the endpoint URL | Enable the listener, preserve raw body/signature headers, verify TLS/path forwarding, and bind the public key from the same application. |
provider.identity reports an application mismatch | The bot token belongs to a different application, or application_id is wrong. Correct the credential set together. |
| Bot is installed but messages create no AGH route | Configure the supported Application Webhook Events subscription; Gateway events are not consumed by this provider. |
| Commands reach Discord but not AGH | Confirm the command is registered to this application and its interaction endpoint is the AGH public URL. |
| Ordinary message edits create no new turn | This is expected: MESSAGE_UPDATE is outside the HTTP-only provider contract. |
| Discord returns missing-access or missing-permission | Check bot installation, channel overrides, requested permission bits, and the target channel/thread IDs. |
| Replies appear outside the thread | The wizard default is group-only. Use a group-plus-thread policy before ingest, then confirm both IDs in agh bridge routes. |
| Long messages fail near 2,000 characters | Check the actual prepared payload and provider response; AGH already reserves continuation markers within each 2,000-code-point chunk. |
| Reactions/typing fail while text still works | Treat them as optional affordances; disable them while repairing permissions or unsupported target behavior. |
| A second Discord instance degrades | Use one shared listener address and a distinct path per bridge instance. |
Security notes
- Treat the bot token as a password and rotate it immediately after exposure.
- Preserve raw request bytes until Ed25519 verification succeeds.
- Limit installation and channel permissions to the server areas the agent should reach.
- Configure DM policy before allowing broad bot discovery.
- Do not add a generic unauthenticated webhook in front of the signed provider endpoint.
Completion checklist
- The Discord extension is installed, enabled, and healthy in the provider catalog.
- Discord accepts the signed PING at the exact public URL.
- The bot token identity matches the configured application ID.
- A direct-DM bridge reports the intended
dm_policyand sender lists before enablement. - A supported webhook event or interaction creates an AGH route.
- If thread-preserving routing was selected, server routes contain both parent and thread IDs.
send-testreturns a Discord remote message ID.- Long output produces ordered messages within Discord's limit.
- The bridge returns to
readyafter one restart.