Skip to content
AGH RuntimeBridges

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

SurfaceAGH behavior
Inbound transportReceives Ed25519-signed interaction payloads and Application Webhook Events over public HTTPS.
Routed eventsMaps MESSAGE_CREATE, reaction add/remove, application commands, and message components into typed bridge events.
Event boundaryDoes not ingest ordinary MESSAGE_UPDATE; that event requires a Discord Gateway lifecycle outside this provider.
RoutingMaps guild channels, threads, and DMs into distinct dimensions; the instance policy decides which dimensions become the session and reply target.
Outbound deliveryCreates, edits, and deletes Discord messages through REST.
Long responsesSplits terminal text at 2,000 Unicode code points with ordered, numbered continuations.
Tool progressDefaults 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

ValueRequiredWhere it comes from
Application IDyesDiscord Developer Portal General Information.
Ed25519 public keyyesDiscord Developer Portal General Information; exactly 64 hexadecimal characters.
Bot tokenyesDiscord Developer Portal Bot page.
Public callback URLyesStable public HTTPS route to the Discord provider listener.
Local listener addressyesprovider_config.webhook.listen_addr or AGH_BRIDGE_DISCORD_LISTEN_ADDR.
AGH workspace IDyesWorkspace whose default agent receives accepted Discord events.
Server permissionsyesPermission 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

  1. Open the Discord Developer Portal.
  2. Select New Application and choose a recognizable name.
  3. On General Information, copy the Application ID and Public Key.
  4. Open Bot, create the bot user if needed, and reset/copy the bot token.
  5. 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/support

The proxy must preserve the raw body and these headers:

  • X-Signature-Timestamp
  • X-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 discord

The 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: bot and applications.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
}
JSON

Unknown 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_123

Choose one routing shape

Route dimensions are conjunctive: every selected dimension must exist on an inbound event.

Intended trafficrouting_policyResult
Guild channels and their threadsinclude_group=trueOne session and reply target per parent channel; this is the wizard default.
Server threads onlyinclude_group=true, include_thread=trueOne session and reply target per thread; ordinary channel events fail routing.
Direct messages onlyinclude_peer=trueOne 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:

  1. Set Interactions Endpoint URL to the exact webhook_public_url returned by setup.
  2. Save the endpoint. Discord sends a signed PING; AGH validates Ed25519 and returns PONG.
  3. Configure Application Webhook Events you intend to route, using the same endpoint.
  4. 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

  1. Open the invite_url returned by setup.
  2. Select the test server.
  3. Review the requested permissions.
  4. Authorize the application.
  5. 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" --json

Expected evidence:

  • provider.identity passes when the bot token resolves to the configured application/bot ID;
  • webhook.public_key passes when the Ed25519 public-key binding exists;
  • webhook reachability is skipped while 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 json

Then 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 json

With 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 \
  --json

For 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 \
  --json

Confirm 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=false

Configuration reference

FieldDefaultPurpose
application_idnoneExpected identity for the authenticated bot token and invite generation.
webhook.public_urlnoneComplete public Discord endpoint and enabled reachability target.
webhook.listen_addrAGH_BRIDGE_DISCORD_LISTEN_ADDRLocal listener shared by Discord instances.
webhook.path/discord/<bridge-id>Local callback path; each instance needs a unique path.
invite.scopesguided setup valuesOAuth scopes used to construct the operator handoff URL.
invite.permissions68672 in guided setupDecimal Discord permission bitset placed in the invite URL.
batching.delay_ms0Enables inbound batching when positive.
batching.split_delay_msdelay_msDelay after the configured split threshold.
batching.split_threshold0Provider batching split threshold.
dm.allow_user_ids / paired_user_idsemptyDiscord IDs admitted by allowlist or pairing.
dm.allow_usernames / paired_usernamesemptyNormalized 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" --json

Use 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

SymptomWhat to check
Discord rejects the endpoint URLEnable 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 mismatchThe 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 routeConfigure the supported Application Webhook Events subscription; Gateway events are not consumed by this provider.
Commands reach Discord but not AGHConfirm the command is registered to this application and its interaction endpoint is the AGH public URL.
Ordinary message edits create no new turnThis is expected: MESSAGE_UPDATE is outside the HTTP-only provider contract.
Discord returns missing-access or missing-permissionCheck bot installation, channel overrides, requested permission bits, and the target channel/thread IDs.
Replies appear outside the threadThe 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 charactersCheck 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 worksTreat them as optional affordances; disable them while repairing permissions or unsupported target behavior.
A second Discord instance degradesUse 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_policy and 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-test returns a Discord remote message ID.
  • Long output produces ordered messages within Discord's limit.
  • The bridge returns to ready after one restart.

On this page