Skip to main content
This bot answers a handful of ! commands, and each one exercises a different corner of the SDK: reactions, message history, the moderation actions behind the guild() handle, the two halves of a mention, and the typed error the server sends back when it says no. The SDK repo ships this as examples/mod-bot.ts. Everything below is server-scoped, so the very first thing the message handler does is drop direct messages.

The full bot

mod-bot.ts

Run it

Then, in any channel the bot can see: !ping, !roles, !bans, !history 50, !purge 5, !timeout <userId> 600, !pin as a reply, !announce <text>, or just @ the bot.

How it works, piece by piece

messageCreate is unified: direct messages arrive on the same event with isDM: true and a null serverId. Every command in this bot is server-scoped, so if (msg.isDM || !msg.serverId) return; runs before anything else.Without it, client.guild(msg.serverId) does not typecheck, and in plain JavaScript the bot builds a handle around null and every action misfires. Any handler that touches serverId, can(), or guild() needs this guard. See Direct messages.
client.guild(serverId) binds a server id onto every server-scoped method: fetchRoles(), fetchBans(), fetchMessages(), timeout(), send(), and the rest. It is pure sugar over the flat client surface, with no cache and no state, so creating one per event costs nothing. The Message objects it hands back are fully actionable, which is how !purge and !pin reach delete() and pin(). See The guild() handle.
msg.mentionsMe is true when the decrypted body carries a <@id> entity naming this bot. It covers direct mentions only: @everyone and @here land on msg.mentions.everyone and msg.mentions.here, and a role mention surfaces as a plain name in msg.mentions.names, because the SDK does not know which roles the bot holds.Sending a mention takes two pieces that the SDK deliberately never infers from each other:
  • userMention(id) produces the pill, which is literal text inside the encrypted body.
  • { mentions: [{ userId: id }] } produces the ping, which is what the server routes the notification from.
Text with no ping shows a pill and notifies nobody. A ping with no text notifies someone and shows nothing. Send both.
Mention targets ride the wire in plaintext. Message bodies are end-to-end encrypted, but the server has to read the mention array to fan notifications out, so anyone with server-side visibility can see who your bot pinged, just not what it said. See What is encrypted, and what is not.
See Mentions and replies.
Any action here can be refused. can() is advisory and the server re-checks everything, so a call that looked fine locally can still come back denied. The SDK rejects with a CloakActionError carrying opcode, code, message, and permission when the denial maps to one.This bot replies the denial into the channel rather than only logging it, so the person who typed the command learns why it failed. Anything that is not a CloakActionError falls through to console.error, because that is a bug rather than a policy decision. See Handling errors.
!purge is many round trips, so it shows the typing dots while it works. Three rules matter:
  • Receivers clear the indicator themselves about 10 seconds after the last true, so re-assert it inside the loop. Repeats of the same value inside the SDK’s roughly one second throttle are dropped without touching the wire.
  • Clear it with sendTyping(serverId, channelId, false) in a finally. An indicator left on after a crash lingers for the full timeout, and clearing it is the entire reason the false argument exists.
  • sendTyping() moves the same selection cursor a send() does, and like send() it is fire-and-forget: a resolved promise means the frame went out.
See Typing, presence, and profile.
fetchMessages() returns a page with the newest message last, so !purge reverses it and deletes from the newest end. The command message you typed is too new to appear in that page, so it is never part of the batch and stays put. The cap of 50 keeps a single typo from clearing a channel. Deleting someone else’s message needs message_manage. See Moderation.
Lower position values sort higher, and position can be null, so the sort is ascending with a fallback: (a.position ?? 99) - (b.position ?? 99). fetchRoles() is the healthy read on this surface. Each Role carries id, name, color, secondColor, position, and memberCount. See Members and roles.
The shipped example still carries a !members command built on fetchMembers(). It is left out here on purpose.
fetchMembers() and guild().fetchMembers() are broken against every current backend. Server rosters moved to opcodes this SDK does not speak yet, so a server-scoped request gets no reply frame at all, not even an error code. The promise neither resolves nor rejects: await hangs until the transport timeout, and try/catch does not save you. It looks exactly like the bot froze. The signature will survive the fix, so code written against it stays valid, but do not ship it today.
fetchRoles() is unaffected. If you need to check a name against the server’s roles, that is the call to use.
setStatus('online') writes an account-level base status to the server, so it survives reconnects, restarts, and redeploys. Set it at ready and leave it alone. Do not re-send it on a timer.Base online means “defer to liveness”: a bot with no live session displays as offline whatever its base says. The other four values (invisible, away, busy, dnd) pin the display, and dnd also suppresses push on every device.
messageDelete, pinUpdate, reactionUpdate, and channelCreate report what the server broadcasts, and they fire for the bot’s own actions too. When !purge deletes, you see the deletes. When !pin reacts, you see the reaction. Guard against reacting to your own effects if one of these handlers ever does real work.typing is different in one non-obvious way: its serverId and channelId are always null. The relay frame carries no routing at all, so there is genuinely nothing to read. Do not substitute the bot’s own selection cursor, which points wherever the bot last sent and has nothing to do with who is typing.

Next

Moderation

Bans, timeouts, purges, and pins in depth.

Mentions and replies

The pill, the ping, and true replies.

Handling errors

What CloakActionError carries and how to read it.

Welcome bot

A simpler starting example to compare against.