Discord
Made by log new platform discord. Discord is a platform of its own: every command is log discord <command> …, and each one is an MCP tool named discord<Command>.
What is here
| Part | Where |
|---|---|
| The accounts | Twenty socialAccounts with platform DISCORD (added 2026-10-28 by scripts/twenty/twentySetup.mjs). First account: sourcedandsoldtothehighestbidder, AdsPower profile k1ifqe1s |
| The session (find the account, its gate, its engine, the answer back to Twenty) | src/discord/account/session, on the shared src/platformKit/session |
| The commands | src/cli/discord/index.ts: log discord session check (MCP discordSessionCheck) |
| The engine | python/discordApi/engine.py, on the shared python/platformKit/engine.py; each account's governor is under ~/.logCli/discord/<account> |
| The client | vendor/endcord (sparklost/endcord at d6765b0, matt's pick 2026-10-28 from clients.md): its endcord/discord.py REST client, gateway.py gateway and auth.py sign-in |
| The session | the account's discord.com localStorage token, saved to Twenty (storageOrigins) when its browser closes (savePlatformSession, for every platform but Facebook) and handed to the engine with the cookies (sessionStorage on the platform's shape). Never printed |
| The session | the account's discord.com localStorage token, saved to Twenty (storageOrigins) when its browser closes (savePlatformSession, for every platform but Facebook) and handed to the engine with the cookies (sessionStorage on the platform's shape). Never printed |
| Proxy | none, like Reddit (matt, 2026-10-28): the gate reads this machine's own exit and the profile's timezone follows it |
What the inspect found (2026-10-28, log inspect on sourcedandsoldtothehighestbidder)
Runs ~/.logCli/runs/inspect/20261028T062914_… (sign-in, chats, paging) and 20261028T065202_… (the gateway push).
Signing in
discord.ggis the invite domain.discord.gg/loginreadsloginas an invite code:GET /api/v9/invites/loginanswers 404Unknown Invite(code 10006) and the page says "Invite Invalid". The login page ishttps://discord.com/login.- matt typed his details himself; pressing Log In sent
POST /api/v9/auth/login→ 200, and the page went todiscord.com/app, thendiscord.com/channels/@me("Friends"). No captcha, no two-step on this account. - The page-state map calls Discord's login page
loginForm, a Facebook state whose resolver is Facebook's auto sign-in. It does not apply here; the sign-in is matt's.
Finding a chat
- Ctrl+K opens the Quick Switcher (
combobox "Quick Switcher"). Typing a name searches what the client already holds: the only call it makes isGET /api/v9/games/autocomplete?q=<text>. People, DMs and channels come back asoptionrows (Jilly, User, Jilly#0902); Enter opens the selected one.
Reading a chat's history
GET /api/v9/channels/<channel_id>/messages?before=<oldest loaded message id>&limit=20Scrolling up (PageUp in the chat) fires it once the loaded messages run out. It answers the 20 messages before before, newest first; the oldest id in that answer is the next before. Fewer than 20 back means the start of the chat.
Sending
POST /api/v9/channels/<channel_id>/messages
{"content": "...", "nonce": "<snowflake>", "tts": false, "flags": 0, "mobile_network_type": "unknown"}
→ 200 {"id": "...", "channel_id": "...", "author": {...}, "timestamp": "...", ...}The answer's id is the message. In a server channel a mention is <@user_id> in content and the user in mentions (VIBE BOT in vibe coders university #bot-help, <@1458086860571017340>).
How a reply comes in: the gateway pushes it
Discord never polls. One WebSocket, wss://gateway.discord.gg/?encoding=json&v=9&compress=zlib-stream, carries everything the account sees, as JSON events compressed in one continuous zlib stream (one inflater per socket; a message ends with 00 00 ff ff). Each event is {"op", "t", "s", "d"}: t names it, s is its sequence.
One real round trip, DM with Jilly (channel 1555254123148873869), 28 Oct:
| UTC | ||
|---|---|---|
| 06:55:08.337 | HTTP | POST …/messages → 200, our message |
| 08.774 (+0.44 s) | RECV | MESSAGE_CREATE our own message, echoed back |
| 08.974 (+0.64 s) | RECV | TYPING_START Jilly (user 1540556850045521930) |
| 09.117 (+0.78 s) | RECV | MESSAGE_REACTION_ADD Jilly on our message |
| 11.600 (+3.3 s) | RECV | TYPING_START |
| 12.521 (+4.2 s) | RECV | MESSAGE_CREATE Jilly's reply ("Through AgentConnect's Discord integration…") |
| 15.853 (+7.5 s) | RECV | MESSAGE_ACK the client marks it read |
The client then fetches the sender's profile (GET /api/v9/users/<id>/profile) to draw it, and acks (POST …/messages/<id>/ack). Other events seen on the same socket: MESSAGE_UPDATE, MESSAGE_DELETE, PRESENCE_UPDATE, PASSIVE_UPDATE_V2, CHANNEL_UPDATE, THREAD_CREATE, VOICE_STATE_UPDATE, SESSIONS_REPLACE, and the heartbeat (op 1 sent, op 11 back).
So a Discord watch is the X Chat watch again (docs/platforms/x/xchat/live.md): one socket, push, nothing polled. A MESSAGE_CREATE whose d.channel_id is a DM and whose d.author.id isn't the account is a new message to answer.
Recording it
log inspect now records every WebSocket frame, sent and received, into each step's network.har (_webSocketMessages on a 101 entry, the browser's own HAR shape; python/browser/harRecorder.py SocketTap, which inflates zlib-stream and redacts a token in the socket URL). Each frame is an exchange named for its event (RECV ws MESSAGE_CREATE) in exchanges.json / exchanges.md, beside the HTTP calls. A step's SubQ reading keeps the first 3 frames of each event and folds the rest into one count (foldSocketFrames), so Discord's hundreds of presence events don't drown it.
Commands on endcord
| Command | MCP | What it does |
|---|---|---|
log discord session check --account <a> | discordSessionCheck | GET /api/v9/users/@me: is the saved token still signed in |
log discord chats --account <a> | discordChats | Every DM and group DM with its id, name and members (/users/<me>/channels; any answer but 200 stops it and says what Discord answered, rule 94) |
log discord send --account <a> --channel "<as matt says it>" --text "…" | discordSend, discordSendLive | One message; the chat is matched the way matt says it (src/discord/chat/match: "the log group", "jilly", "listaro"), refused with the candidates when two fit; a dry run unless --live --approvedBy. The copy is read for what AI chat interfaces leave in text first (rule 111): hidden characters, odd spaces or pasted Claude HTML refuse it with aiArtifacts, and the reading is logged and kept in --json |
log discord read --account <a> --channel <id or name> [--after <id>] | discordRead | A chat or channel's latest messages, oldest first: who, when, bot or not, what it replies to, text and embeds; --after keeps what came after a message (a bot's answer to ours) |
log discord watch --account <a> [--channel …] [--seconds <s>] [--inbox] | discordWatch | endcord's gateway (wss://gateway.discord.gg): every message printed the moment Discord pushes it, nothing polled. --inbox hands each DM, group message, mention of the account or reply to it (never a bot's "Thinking..." placeholder) to the inbox hook |
log search index --platform discord | searchIndex | Every DM and group's messages, up to 20,000 each (paged 100 at a time with before; a 429 or any other refusal from Discord stops the whole read at that conversation: the answer names it and what Discord answered, the conversations read before it are kept and indexed, nothing more is read or retried, and a 429 pauses the account, rule 94; a network drop only notes that conversation and goes on), kept in ~/.logCli/conversations/discord/<account> and indexed with X's for log search (rule 89) |
The inbox hook (scripts/hooks/inbox.ts) keeps one log discord watch --inbox running for every signed-in Discord account, as it does log twitter chat watch --inbox for X, and puts each new message in front of the agent as >>> NEW DISCORD MESSAGE, with how to read the chat, draft from matt's guide, gate it with log voice and send only when matt says. Tested 2026-10-28: VIBE BOT's answer in #bot-help reached the agent through the hook with no read.
First full read, 2026-10-28: 176 conversations, 13,666 messages; with X's, 4,819 chunks. log search bench: warm p50 4.7 ms, p99 8.1 ms, against 66.7 ms for a plain scan.
On GitHub
Every call and event above was searched on GitHub (2026-10-28): githubSearch.md. All of them are known there; discord-userdoccers/discord-userdoccers documents most, and chojs23/concord, sparklost/endcord and Discord-S.C.U.M already run the gateway watch and the send. Each of the 7 clients was cloned and counted against the 66 things we found: clients.md.
Next
- A watch on the gateway:
log discord chat watch --account <a>in the account's open browser (or a vendored client), eachMESSAGE_CREATEto someone else handed to the inbox hook, aslog twitter chat watch --inboxdoes. - Read and send as commands, on the two calls above, each in the account's open browser (rule 35), sending only with
--live. - Done 2026-10-28: endcord is vendored and
ping(GET /api/v9/users/@me) runs on it:log discord session checkanswered loggedIn as sourcedandsoldtothehighestbidder. Next on it: the gateway watch fromendcord/gateway.py, then read and send fromendcord/discord.py. - A
discordLoginpage state, so the hook stops offering Facebook's auto sign-in on Discord's login page.