The bot includes an optional REST API for programmatic access to Discord conversation history, user information, and live LLM context.
In production, this is the Bridge bot at borgs.animalabs.ai:3306 β the only chapterx bot launched with API_BEARER_TOKEN and API_PORT env vars. The infra bot's /get_prompt and /get_context slash commands proxy to it via CHAPTERX_API_URL / CHAPTERX_API_TOKEN.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/health |
GET | No | Health check |
/api/channels |
GET | Yes | List channels the bot has access to, with last-activity timestamps |
/api/channels/:channelId/latest |
GET | Yes | Get the latest N raw messages for a channel |
/api/messages/export |
POST | Yes | Export conversation history with a boundary message URL (follows .history) |
/api/context/build |
POST | Yes | Build live LLM context for a channel (full prompt pipeline) |
/api/users/:userId |
GET | Yes | Get user info (username, display name, roles, avatar) |
/api/users/:userId/avatar |
GET | Yes | Get user avatar CDN URL |
- Generate a secure API token:
openssl rand -hex 32 > api_token- Set environment variable:
export API_BEARER_TOKEN=$(cat api_token)
export API_PORT=3000 # Optional, defaults to 3000- Start the bot:
npm run devThe API server will start alongside the bot and log: "API server started" { port: 3000 }. If API_BEARER_TOKEN is not set, the API is disabled ("API server disabled (no API_BEARER_TOKEN set)").
In production, the Bridge bot is the canonical API-enabled chapterx bot. Its borgs-admin start command is:
API_BEARER_TOKEN=<bridge-token> API_PORT=3306 EMS_PATH=/opt/chapter2/ems BOT_NAME=Bridge npx tsx src/main.tsThe infra bot (LuxiaSL/infra) calls Bridge for /get_prompt and /get_context via CHAPTERX_API_URL=http://localhost:3306 + CHAPTERX_API_TOKEN. Direct access from outside borgs requires the same bearer token.
GET /healthNo authentication required.
Response:
{
"status": "ok",
"timestamp": "2025-11-14T23:00:00.000Z"
}Example:
curl http://localhost:3000/healthGET /api/channels?guildId=GUILD_ID&since=ISO_TIMESTAMPList every text channel (and thread) the bot has access to, with last-activity timestamps derived from Discord snowflakes β no message fetches required, so this is fast even with hundreds of channels.
Authentication: Bearer token required
Query parameters:
guildId(optional): Filter to a single guildsince(optional): ISO 8601 timestamp β only include channels with activity since this time
Response:
{
"channels": [
{
"id": "1398036481146355803",
"name": "meme-factory",
"guildId": "1391260973872185424",
"guildName": "Cyborgism",
"type": 0,
"isThread": false,
"parentId": "1391260974278422571",
"lastMessageId": "1515013700112617475",
"lastActivityAt": "2026-06-13T01:23:42.118Z"
},
{
"id": "1420468671452676211",
"name": "basin",
"guildId": "1391260973872185424",
"guildName": "Cyborgism",
"type": 0,
"isThread": false,
"parentId": "1391260974278422571",
"lastMessageId": "1515013214521794048",
"lastActivityAt": "2026-06-13T01:21:46.327Z"
}
],
"metadata": {
"count": 482,
"guildCount": 12,
"guildIdFilter": null,
"since": null
}
}Notes:
- Channels are sorted by
lastMessageIddescending (newest activity first); channels with no messages sink to the bottom. typeis the discord.jsChannelTypeenum value (0 = text, 5 = announcement, 11 = public thread, etc.).- Voice and category channels are skipped.
lastActivityAtis derived from the snowflake (no API call) β accurate to the millisecond the message was created.
Example:
# All channels
curl "http://localhost:3306/api/channels" \
-H "Authorization: Bearer $(cat api_token)"
# Channels in one guild, active in the last hour
curl "http://localhost:3306/api/channels?guildId=1391260973872185424&since=$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)" \
-H "Authorization: Bearer $(cat api_token)"GET /api/channels/:channelId/latest?messages=N&maxImages=N&ignoreHistory=BOOLGet the latest N raw messages for a channel, in the same shape as /api/messages/export (full metadata, base64 attachments, reactions, reply references). Unlike export, no boundary message URL is required β this just returns the tail.
Authentication: Bearer token required
Path parameters:
channelId(required): Discord channel ID (numeric)
Query parameters:
messages(optional): Number of messages to return. Default 50, max 500.maxImages(optional): Maximum number of image attachments to download and base64-encode. Default 50, 0 to skip.ignoreHistory(optional): Skip.historycommand processing. Defaulttrue(raw export). Setfalseto follow.historyredirects, matching bot inference behavior.
Response: Same shape as /api/messages/export, with channel-specific metadata:
{
"messages": [ /* ...same as export... */ ],
"metadata": {
"channelId": "1398036481146355803",
"firstMessageId": "1515013214521794048",
"lastMessageId": "1515013700112617475",
"totalCount": 50,
"requestedCount": 50,
"ignoreHistory": true
}
}Errors:
- 400 β
channelIdis not numeric - 403 β bot lacks permission to view the channel
- 404 β channel not found, bot not a guild member, or channel empty
Example:
# Latest 50 (default) in meme-factory
curl "http://localhost:3306/api/channels/1398036481146355803/latest" \
-H "Authorization: Bearer $(cat api_token)"
# Latest 200, no images
curl "http://localhost:3306/api/channels/1398036481146355803/latest?messages=200&maxImages=0" \
-H "Authorization: Bearer $(cat api_token)"POST /api/messages/exportExport Discord conversation history with full metadata. Automatically processes .history commands found in channels (uses unified traversal logic with the bot).
Authentication: Bearer token required in Authorization header
Request Body:
{
"last": "https://discord.com/channels/GUILD_ID/CHANNEL_ID/MESSAGE_ID",
"first": "https://discord.com/channels/GUILD_ID/CHANNEL_ID/MESSAGE_ID",
"recencyWindow": {
"messages": 400,
"characters": 100000
},
"ignoreHistory": false
}Parameters:
last(required): Discord message URL - end point of rangefirst(optional): Discord message URL - start point of rangerecencyWindow(optional): Limits to applymessages: Maximum number of messages (default: 50)characters: Maximum total characters- If omitted entirely, defaults to 50 messages
ignoreHistory(optional): Skip.historycommand processing (default: true)- When
true(default), fetches raw messages without following.historyredirects or clearing context - Set to
falseto process.historycommands during traversal (matches bot inference behavior)
- When
Response:
{
"messages": [
{
"id": "1234567890",
"author": {
"id": "9876543210",
"username": "alice",
"displayName": "Alice",
"bot": false
},
"content": "Hello world!",
"timestamp": "2025-11-14T23:00:00.000Z",
"reactions": [
{"emoji": "π", "count": 3},
{"emoji": "β€οΈ", "count": 1}
],
"attachments": [
{
"id": "...",
"url": "https://cdn.discord.com/...",
"filename": "image.png",
"contentType": "image/png",
"size": 123456,
"base64Data": "iVBORw0KGgoAAAANSUhEUgAA...",
"mediaType": "image/png"
}
],
"referencedMessageId": "1234567889"
}
],
"metadata": {
"channelId": "1234567890",
"guildId": "9876543210",
"firstMessageId": "1234567880",
"lastMessageId": "1234567890",
"totalCount": 100,
"truncated": false
}
}POST /api/context/buildBuild the full live LLM context for a channel by running the bot's complete context-builder pipeline (config + steering + plugin injections + tool cache + message merging + recency window + cache markers + stop sequences). Equivalent to a "what would the bot's prompt look like right now?" inspection β the same construction used for an actual activation, minus the LLM call.
Bot config takes effect: pinned channel configs, guild overrides, system prompt, mode, model, and stop sequences all match what the bot would use to respond. Character limits are disabled so you can request large windows; the configured recency_window_messages becomes the default if messages is omitted.
Authentication: Bearer token required
Request Body:
{
"channel": "https://discord.com/channels/GUILD_ID/CHANNEL_ID",
"messages": 500
}Parameters:
channel(required): Discord channel URL or numeric channel IDmessages(optional): Number of messages to include. Defaults to the bot's configuredrecency_window_messages(typically 200).recency_window_charactersandhard_max_charactersare forced toInfinityso this is the only effective cap.
Response:
{
"messages": [
{
"participant": "Alice",
"content": "Hello world!",
"hasImages": false,
"imageCount": 0
},
{
"participant": "Claude",
"content": "Hi Alice!",
"hasImages": false,
"imageCount": 0
}
],
"metadata": {
"botName": "Claude Fable",
"channelId": "1398036481146355803",
"messageCount": 487,
"discordMessagesFetched": 600,
"configuredLimit": 500,
"requestedLimit": 500,
"model": "claude-fable-5",
"mode": "chat",
"systemPrompt": "You are Claude Fable 5. You are connected to Discord...",
"contextPrefix": null,
"stopSequences": [
"\nAlice:",
"\nClaude Fable:",
"<<HUMAN_CONVERSATION_END>>"
]
}
}Response fields:
messages[].participantβ name as it appears in the LLM prompt (after display-name resolution, character overrides, etc.)messages[].contentβ text content joined with newlines (images noted separately)messages[].hasImages/imageCountβ image attachments not included incontentmetadata.discordMessagesFetchedβ raw count returned by Discord (always β₯messageCountsince merging/filtering reduces)metadata.configuredLimitβ what the bot's YAML config says (independent of themessagesparameter)metadata.requestedLimitβ themessagesvalue from the request, ornullif defaultedmetadata.systemPrompt/contextPrefixβ first 200 chars only, with ellipsis if longermetadata.stopSequencesβ exact stop sequences this request would send to the LLM
Errors:
- 400 β missing
channel, invalid URL/ID format - 403 β bot lacks access to channel
- 404 β channel not found, or empty channel
- 501 β API server lacks the context builder (Bridge is configured for it; other API-enabled bots may not be)
Example:
curl -X POST http://localhost:3306/api/context/build \
-H "Authorization: Bearer $(cat api_token)" \
-H "Content-Type: application/json" \
-d '{
"channel": "https://discord.com/channels/1391260973872185424/1398036481146355803",
"messages": 500
}'Notes:
- Returns the prompt as Bridge would assemble it. Bots with different system prompts or steering still produce different prompts at activation time β use the bot whose context you want.
- No LLM call is made; this is read-only.
- Tool cache is empty in the returned context (the bot's actual activations interleave tool history; this endpoint is for prompt inspection, not replay).
GET /api/users/:userId?guildId=GUILD_IDGet information about a Discord user, optionally with server-specific details.
Authentication: Bearer token required
Parameters:
userId(path, required): Discord user IDguildId(query, optional): Guild ID for server-specific display name and roles
Response:
{
"id": "1030846477418909696",
"username": "q_m_o",
"displayName": "Egr. Catalyst",
"discriminator": "0",
"bot": false,
"avatarUrl": "https://cdn.discordapp.com/avatars/.../....webp?size=128",
"roles": ["mod", "alpha tester", "bot orchestrator"]
}Notes:
- Without
guildId: Returns username as displayName, no roles field - With
guildId: Returns server-specific nickname and role list rolesarray excludes @everyone
Example:
# Global user info
curl "http://localhost:3000/api/users/1030846477418909696" \
-H "Authorization: Bearer your-token-here"
# Server-specific info
curl "http://localhost:3000/api/users/1030846477418909696?guildId=1052321771216457748" \
-H "Authorization: Bearer your-token-here"GET /api/users/:userId/avatar?size=SIZEGet CDN URL for a user's avatar image.
Authentication: Bearer token required
Parameters:
userId(path, required): Discord user IDsize(query, optional): Avatar size in pixels (128, 256, 512, 1024)- Default: 128
Response:
{
"avatarUrl": "https://cdn.discordapp.com/avatars/USER_ID/HASH.png?size=256"
}Example:
curl "http://localhost:3000/api/users/1030846477418909696/avatar?size=256" \
-H "Authorization: Bearer your-token-here"import requests
# Export messages
response = requests.post(
'http://localhost:3000/api/messages/export',
headers={'Authorization': 'Bearer your-token-here'},
json={
'last': 'https://discord.com/channels/123/456/789',
'recencyWindow': {'messages': 400}
}
)
data = response.json()
for msg in data['messages']:
print(f"{msg['author']['displayName']}: {msg['content']}")
# Get user info
user_id = data['messages'][0]['author']['id']
user_info = requests.get(
f'http://localhost:3000/api/users/{user_id}?guildId=123',
headers={'Authorization': 'Bearer your-token-here'}
).json()
print(f"User: {user_info['displayName']}")
print(f"Roles: {', '.join(user_info['roles'])}")# Export messages
curl -X POST http://localhost:3000/api/messages/export \
-H "Authorization: Bearer $(cat api_token)" \
-H "Content-Type: application/json" \
-d '{
"last": "https://discord.com/channels/123/456/789",
"first": "https://discord.com/channels/123/456/700",
"recencyWindow": {"messages": 400}
}'
# Get user info
curl "http://localhost:3000/api/users/1030846477418909696?guildId=123" \
-H "Authorization: Bearer $(cat api_token)"
# Get avatar
curl "http://localhost:3000/api/users/1030846477418909696/avatar?size=512" \
-H "Authorization: Bearer $(cat api_token)"- Live prompt inspection -
/api/context/buildreturns what the bot would send to the LLM right now, without making the call. Backs/get_promptand/get_contextslash commands. - Research Commons Integration - Export Discord conversations for web archive with full user metadata
- Data Analysis - Extract conversation data with reactions, attachments, and user info
- Backup/Archive - Programmatic conversation backups with complete metadata
- User Directory - Build user profiles with server-specific names, roles, and avatars
- Frontend Display - Get avatar URLs and display names for UI rendering
- β
Unified
.historysupport - Same traversal logic as bot - β Reactions included - Emoji and count for each reaction
- β Attachments with base64 - Full metadata (URL, filename, size, type) + base64-encoded image data
- β Image type detection - Magic byte detection for accurate MIME types
- β Reply tracking - referencedMessageId for threaded conversations
- β Cross-channel/server - Works with any Discord URL
- β Recency limits - Message count or character limits (default: 50 messages)
- β Optimized fetching - Only fetches what's needed based on limits
- β Full pipeline - Runs the actual bot context builder (config, steering, plugins, merging, recency, cache markers, stop sequences)
- β Per-bot config - Pinned configs, system prompt, mode, model all match the running bot
- β
No character cap -
recency_window_characters/hard_max_charactersforced toInfinity;messagesis the only cap - β Read-only - No LLM call made
- β Global user info - Username, discriminator, bot flag
- β Server-specific - Display name and roles per guild
- β Avatar URLs - CDN links with configurable size
- β Space for mapping - Future author aliasing support
The API returns appropriate HTTP status codes with detailed error messages:
| Code | Meaning | Example |
|---|---|---|
| 200 | Success | Request completed successfully |
| 400 | Bad Request | Invalid URL format, missing parameters |
| 401 | Unauthorized | Missing authorization header |
| 403 | Forbidden | Invalid bearer token |
| 404 | Not Found | User/channel/message not found |
| 500 | Server Error | Unexpected server error |
{
"error": "Not Found",
"message": "Channel 123456789 not found or bot is not a member of this guild",
"details": "The bot cannot access this channel/message. Check bot permissions."
}Invalid URL Format (400)
{
"error": "Bad Request",
"message": "Invalid Discord message URL format",
"details": "Expected format: https://discord.com/channels/GUILD_ID/CHANNEL_ID/MESSAGE_ID"
}Channel Not Accessible (403/404)
{
"error": "Forbidden",
"message": "Missing Access: Bot does not have permission to view channel 123456789",
"details": "The bot does not have permission to access this channel."
}Message Not Found (404)
{
"error": "Not Found",
"message": "Unknown Message: Message 123456789 not found in channel 987654321",
"details": "The bot cannot access this channel/message. Check bot permissions."
}User Not Found (404)
{
"error": "Not Found",
"message": "User 123456789 not found",
"details": "The user may not exist or the bot cannot see them."
}Invalid Authentication (401/403)
{
"error": "Invalid bearer token"
}- β
Bearer token authentication (all endpoints except
/health) - β Bot must have access to requested channels
- β Respects Discord permissions (bot's view of channel)
- β Detailed error messages for debugging
β οΈ Keepapi_tokenfile secureβ οΈ Consider rate limiting for production use
Space reserved for mapping Discord users to participant aliases:
{
"author": {
"id": "123",
"username": "alice",
"displayName": "Alice",
"mappedParticipant": "Researcher_A" // Future feature
}
}Configuration will be added to allow anonymization/aliasing for research/export purposes.