Agent MessengerAgent Messenger
TypeScript SDK

Discord Bot

TypeScript SDK reference for Discord Bot — client, real-time Gateway listener, credential management, and types.

Installation

npm install agent-messenger
import {
  DiscordBotClient,
  DiscordBotCredentialManager,
  DiscordBotError,
  DiscordBotListener,
} from 'agent-messenger/discordbot'

DiscordBotClient

The main client for interacting with Discord's REST API using a bot token. All methods include automatic rate-limit handling with per-bucket and global rate-limit tracking.

import { DiscordBotClient } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token: 'YOUR_BOT_TOKEN' })

Or use stored credentials — credentials are read from ~/.config/agent-messenger/discordbot-credentials.json (managed by agent-discordbot auth set):

const client = await new DiscordBotClient().login()

Authentication

// Verify bot credentials and get the bot's identity
const me = await client.testAuth()
// → DiscordUser { id, username, global_name?, avatar?, bot? }

Servers (Guilds)

// List all servers the bot has been added to
const guilds = await client.listGuilds()
// → DiscordGuild[]

// Get a specific server by ID
const guild = await client.getGuild(guildId)
// → DiscordGuild { id, name, icon?, owner? }

Channels

// List all channels in a server
const channels = await client.listChannels(guildId)
// → DiscordChannel[]

// Get a specific channel by ID
const channel = await client.getChannel(channelId)
// → DiscordChannel { id, guild_id, name, type, topic?, position?, parent_id?, thread_metadata? }

// Resolve a channel by name or ID — returns the channel ID
const channelId = await client.resolveChannel(guildId, '#general')
const sameId = await client.resolveChannel(guildId, '1234567890123456789')

Messages

// Send a message to a channel
const msg = await client.sendMessage(channelId, 'Hello world')

// Send a message in a thread
const reply = await client.sendMessage(channelId, 'Reply', { thread_id: threadId })
// → DiscordMessage { id, channel_id, author, content, timestamp }

// Get recent messages from a channel (default limit: 50)
const messages = await client.getMessages(channelId)
const limited = await client.getMessages(channelId, 25)
// → DiscordMessage[]

// Get a single message by ID
const message = await client.getMessage(channelId, messageId)
// → DiscordMessage

// Edit a message (bot can only edit its own messages)
const edited = await client.editMessage(channelId, messageId, 'Updated content')
// → DiscordMessage

// Delete a message (bot can delete its own messages or any with Manage Messages permission)
await client.deleteMessage(channelId, messageId)

Reactions

// Add a reaction (use a standard emoji or `name:id` for custom emojis)
await client.addReaction(channelId, messageId, '👍')
await client.addReaction(channelId, messageId, 'custom_emoji_name:123456789')

// Remove the bot's own reaction
await client.removeReaction(channelId, messageId, '👍')

Users

// List members in a server (up to 1000 at a time)
const users = await client.listUsers(guildId)
// → DiscordUser[]

// Get a user by ID
const user = await client.getUser(userId)
// → DiscordUser { id, username, global_name?, avatar?, bot? }

Files

// Upload a file to a channel (by file path)
const file = await client.uploadFile(channelId, '/path/to/report.pdf')
// → DiscordFile { id, filename, size, url, content_type?, height?, width? }

// List file attachments from recent messages in a channel
const files = await client.listFiles(channelId)
// → DiscordFile[]

Threads

// Create a thread in a channel
const thread = await client.createThread(channelId, 'Bug Report: Login Issue')
const configured = await client.createThread(channelId, 'Design Review', {
  auto_archive_duration: 1440, // minutes: 60, 1440, 4320, or 10080
  rate_limit_per_user: 5, // seconds between messages (slow mode)
})
// → DiscordChannel (with thread_metadata)

// Archive (or unarchive) a thread
await client.archiveThread(threadId)
await client.archiveThread(threadId, false) // unarchive
// → DiscordChannel

Real-Time Events

DiscordBotListener connects to Discord's Gateway WebSocket for instant event streaming — messages, reactions, slash command interactions, member changes, and more. No webhook server required.

import { DiscordBotClient, DiscordBotListener } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token })
const listener = new DiscordBotListener(client)

listener.on('connected', (info) => {
  console.log(`Connected as ${info.user.username} (session ${info.sessionId})`)
})

listener.on('message_create', (event) => {
  if (event.author.bot) return
  console.log(`#${event.channel_id} <${event.author.username}>: ${event.content}`)
})

listener.on('interaction_create', (event) => {
  console.log(`Slash command: ${(event.data as { name?: string } | undefined)?.name}`)
})

listener.on('error', (err) => console.error(err))

await listener.start() // connects via Gateway WebSocket
// listener.stop()      // clean shutdown

Intents

Discord Gateway requires intents — a bitfield that opts the bot into specific event categories. The default set includes safe, non-privileged intents for messages, reactions, and typing in guilds and DMs.

To customize intents, pass DiscordIntent flags:

import { DiscordIntent } from 'agent-messenger/discordbot'

const listener = new DiscordBotListener(client, {
  intents:
    DiscordIntent.Guilds |
    DiscordIntent.GuildMessages |
    DiscordIntent.GuildMessageReactions |
    DiscordIntent.DirectMessages |
    DiscordIntent.MessageContent, // privileged — must enable in Developer Portal
})

Privileged intents (MessageContent, GuildMembers, GuildPresences) must be explicitly enabled in the Discord Developer Portal under your bot's settings before they can be requested. Connecting with a privileged intent that is not enabled will close the Gateway with code 4014.

Without MessageContent, the content field on message_create events will be empty for messages that don't mention the bot or arrive in DMs.

Available Events

EventDescription
message_createNew message in a guild channel or DM
message_updateMessage content edited
message_deleteMessage deleted
message_reaction_add / message_reaction_removeReaction added or removed
guild_member_add / guild_member_removeMember joins or leaves a server
presence_updateUser status changed (requires GuildPresences)
typing_startTyping indicator
channel_create / channel_update / channel_deleteChannel lifecycle
guild_create / guild_update / guild_deleteBot added to / updated in / removed from a server
interaction_createSlash command, button, modal, or context menu fired
discord_eventCatch-all — fires for every Gateway dispatch
connected / disconnectedConnection lifecycle
errorConnection or protocol error

Lifecycle Features

  • Auto-reconnect with exponential backoff (1s → 30s, capped) and jitter on heartbeats.
  • Session resume — reconnects use Discord's resume_gateway_url to replay missed events without re-identifying.
  • Heartbeat keepalive with zombie connection detection — if Discord doesn't acknowledge a heartbeat, the connection is closed and re-established.
  • Non-recoverable close detection — connections close cleanly on close codes 4004 (invalid token), 40104014 (sharding/intents misconfiguration). The listener emits error and stops.
  • Session reset on close codes 4007 (invalid sequence) and 4009 (session timed out) — clears local session state and reconnects fresh.
  • Stale-socket safety — every async callback is generation-guarded so a stop+start cycle cannot leave the new connection in a broken state.

DiscordBotCredentialManager

Manages bot credentials stored at ~/.config/agent-messenger/discordbot-credentials.json with 0o600 permissions. Supports multiple bot identities (e.g., a deploy bot and an alerts bot in the same config). The environment variables E2E_DISCORDBOT_TOKEN, E2E_DISCORDBOT_SERVER_ID, and E2E_DISCORDBOT_SERVER_NAME take precedence over file-based credentials.

import { DiscordBotCredentialManager } from 'agent-messenger/discordbot'

const manager = new DiscordBotCredentialManager()
// Custom path: new DiscordBotCredentialManager('/custom/config/dir')
// Load full config from disk (returns defaults if file doesn't exist)
const config = await manager.load()
// → DiscordBotConfig { current, bots, current_server, servers }

// Save full config to disk
await manager.save(config)

// Get the credentials for the current bot (or a specific bot ID)
const creds = await manager.getCredentials()
const specific = await manager.getCredentials('deploy')
// → DiscordBotCredentials | null

// Store credentials for a bot (also marks it as current)
await manager.setCredentials({
  token: 'YOUR_BOT_TOKEN',
  bot_id: 'deploy',
  bot_name: 'Deploy Bot',
})

// Switch the active bot
await manager.setCurrent('alerts')

// List all stored bots with current marker
const bots = await manager.listAll()
// → Array<DiscordBotCredentials & { is_current: boolean }>

// Remove a stored bot
await manager.removeBot('alerts')

// Clear all stored credentials
await manager.clearCredentials()

// Server preference helpers
const serverId = await manager.getCurrentServer()
await manager.setCurrentServer('1234567890123456789', 'My Server')

Types

import type {
  DiscordBotConfig,
  DiscordBotCredentials,
  DiscordBotEntry,
  DiscordBotListenerEventMap,
  DiscordChannel,
  DiscordFile,
  DiscordGatewayChannelEvent,
  DiscordGatewayEvent,
  DiscordGatewayGenericEvent,
  DiscordGatewayGuildEvent,
  DiscordGatewayInteractionEvent,
  DiscordGatewayMemberEvent,
  DiscordGatewayMessageCreateEvent,
  DiscordGatewayMessageDeleteEvent,
  DiscordGatewayMessageUpdateEvent,
  DiscordGatewayPresenceEvent,
  DiscordGatewayReactionEvent,
  DiscordGatewayTypingEvent,
  DiscordGuild,
  DiscordMessage,
  DiscordReaction,
  DiscordUser,
} from 'agent-messenger/discordbot'

Zod Schemas

Runtime-validated schemas for parsing API responses:

import {
  DiscordBotConfigSchema,
  DiscordBotCredentialsSchema,
  DiscordBotEntrySchema,
  DiscordChannelSchema,
  DiscordFileSchema,
  DiscordGuildSchema,
  DiscordMessageSchema,
  DiscordReactionSchema,
  DiscordUserSchema,
} from 'agent-messenger/discordbot'

Constants

import { DiscordGatewayOpcode, DiscordIntent } from 'agent-messenger/discordbot'

DiscordGatewayOpcode.Identify // 2
DiscordGatewayOpcode.Hello // 10

DiscordIntent.Guilds // 1
DiscordIntent.GuildMessages // 1 << 9
DiscordIntent.MessageContent // 1 << 15 (privileged)

DiscordBotError

All client methods throw DiscordBotError on failure. The error includes a code field describing the failure category — useful for retry logic and structured error reporting.

import { DiscordBotClient, DiscordBotError } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token: 'YOUR_BOT_TOKEN' })

try {
  await client.sendMessage('1234567890123456789', 'Hello!')
} catch (err) {
  if (err instanceof DiscordBotError) {
    console.error(`[${err.code}] ${err.message}`)

    if (err.code === 'rate_limited') {
      // Discord returned 429 — back off and retry
    } else if (err.code === 'missing_token' || err.code === 'no_credentials') {
      // Token is missing — re-run auth set
    }
  } else {
    throw err
  }
}

Error codes thrown by the client:

  • missing_tokenlogin() called with an empty token
  • no_credentialslogin() called without args and no stored bot token was found
  • not_authenticated — Method called before login() resolved
  • rate_limited — Discord 429 response; the client retries automatically with Retry-After, then surfaces this if retries are exhausted
  • network_error — Fetch failed (DNS, connection refused, timeout)
  • channel_not_foundresolveChannel() could not find the named channel in the current guild
  • no_attachments — File upload returned 200 but the response had no attachments
  • http_<status> — Discord returned an unexpected status (e.g., http_500) without a structured code in the error body
  • Discord API error codes (numeric, as strings) — When Discord's response body includes a code field (e.g., "50001" for "Missing Access"), it is propagated as-is. See the Discord JSON Error Codes reference.

Examples

Auto-Reply Bot

Listen for messages mentioning the bot and reply with a help message.

import { DiscordBotClient, DiscordBotListener, DiscordIntent } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token })
const me = await client.testAuth()

const listener = new DiscordBotListener(client, {
  intents:
    DiscordIntent.Guilds | DiscordIntent.GuildMessages | DiscordIntent.DirectMessages | DiscordIntent.MessageContent,
})

listener.on('message_create', async (event) => {
  if (event.author.bot) return

  const mentioned = event.content.includes(`<@${me.id}>`)
  if (!mentioned) return

  await client.sendMessage(event.channel_id, `👋 Hey <@${event.author.id}>! Try \`/help\` for a list of commands.`)
})

await listener.start()

Reaction-Driven Approval Workflow

Post a request, then watch for ✅ or ❌ reactions to advance state.

import { DiscordBotClient, DiscordBotListener } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token })
const listener = new DiscordBotListener(client)

const post = await client.sendMessage(channelId, '🚀 Deploy v2.1.0 to production?')
await client.addReaction(channelId, post.id, '✅')
await client.addReaction(channelId, post.id, '❌')

listener.on('message_reaction_add', async (event) => {
  if (event.message_id !== post.id) return
  if (event.user_id === me.id) return

  if (event.emoji.name === '✅') {
    await client.editMessage(channelId, post.id, '🚀 Deploy v2.1.0 — **approved**')
  } else if (event.emoji.name === '❌') {
    await client.editMessage(channelId, post.id, '🚀 Deploy v2.1.0 — **rejected**')
  }
})

await listener.start()

Slash Command Handler

React to slash command interactions in real time. (The bot must register slash commands separately via Discord's REST API.)

import { DiscordBotClient, DiscordBotListener } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token })
const listener = new DiscordBotListener(client)

listener.on('interaction_create', async (event) => {
  const name = (event.data as { name?: string } | undefined)?.name
  if (name !== 'ping') return

  if (event.channel_id) {
    await client.sendMessage(event.channel_id, '🏓 Pong!')
  }
})

await listener.start()

CI Status Notifier

Post a status message at the start and finish of a CI job, with a reaction to indicate state.

import { DiscordBotClient } from 'agent-messenger/discordbot'

const client = await new DiscordBotClient().login({ token })

const start = await client.sendMessage(channelId, `🛠️ Build #${runId} started`)
await client.addReaction(channelId, start.id, '⏳')

try {
  await runBuild()
  await client.removeReaction(channelId, start.id, '⏳')
  await client.addReaction(channelId, start.id, '✅')
  await client.sendMessage(channelId, `✅ Build #${runId} succeeded`)
} catch (err) {
  await client.removeReaction(channelId, start.id, '⏳')
  await client.addReaction(channelId, start.id, '❌')
  await client.sendMessage(channelId, `❌ Build #${runId} failed: ${(err as Error).message}`)
  throw err
}

Multi-Bot Configuration

Use DiscordBotCredentialManager to switch between multiple bots stored in the same config (e.g., separate deploy and alerts bots).

import { DiscordBotClient, DiscordBotCredentialManager } from 'agent-messenger/discordbot'

const manager = new DiscordBotCredentialManager()

// Store credentials for two bots
await manager.setCredentials({ token: 'TOKEN_A', bot_id: 'deploy', bot_name: 'Deploy Bot' })
await manager.setCredentials({ token: 'TOKEN_B', bot_id: 'alerts', bot_name: 'Alerts Bot' })

// Switch to the alerts bot and use it
await manager.setCurrent('alerts')
const creds = await manager.getCredentials()
const client = await new DiscordBotClient().login({ token: creds!.token })

await client.sendMessage(channelId, '🚨 Error rate above threshold')

On this page