!ping with pong in any channel it can see.
Prerequisites
- Node.js
^20.19.0 || >=22.12.0. Check withnode --version. Node 21.x does not qualify, and neither does Node 18. See Install and requirements for why. - A Cloak account. Download Cloak from cloak.chat if you do not have it yet.
- A server where you have the Manage Server permission, so you can add a bot to it.
- A human member on a current Cloak build. Your bot never generates a server’s key. A member’s client wraps it and uploads it, and this SDK produces and accepts only the current HPKE wrap format. If the person handing over the key runs an old client build, every key acquire fails and the SDK names them in a warning.
Steps
1
Install the SDK
In a new project folder, install the package.Nothing compiles. If you want to skip the two optional native extras your bot will not use, run
npm i @cloak-software/bot-sdk --omit=optional instead.2
Create a bot and copy its token
In the Cloak app, open Settings > My Bots, create a bot, and copy the token.The token has the shape
botid.tokenid.secret and is shown only once. It grants full encrypted membership, so treat it like a password. Store it in an environment variable or a secret manager, and never commit it to source control.3
Add the bot to a server
Open the server you want the bot in, then go to Server Settings > Invites > Add a Bot. This requires the Manage Server permission.Two things happen here. A consent screen asks you to approve the permissions the bot declared, and you can approve a subset. Adding the bot also shares that server’s encryption key with it, which is what lets the bot decrypt and send messages there.Declaring a permission is a request, not a grant. See Permissions.
4
Write the bot
Create Add your keystore to
bot.ts with the following. Read the token from an environment variable so it stays out of your code.bot.ts
.gitignore so it never gets committed:.gitignore
5
Run it
Pass the token on the command line and start the bot. This example uses tsx to run TypeScript directly.You should see
Online as ... in your terminal. Now send !ping in any channel the bot can see, and it replies pong.Why the keystore matters
keystorePath is the one line in that file you cannot skip. Your bot’s identity is published write-once on the server. Run once without a keystore and the private half of that identity dies with your process, while the server keeps the public half forever. Members keep wrapping keys to an identity your bot no longer holds, and the next start fails permanently with an error about a rejected publication. The SDK prints an un-suppressed warning at the moment it happens.
On a host with no writable disk, pass a keystore adapter instead of a path. Keys and the keystore has the full model and the adapter options.
Two behaviors worth learning now
send() is fire-and-forget. The promise resolves when the frame goes out on the wire, not when the server accepts it. The protocol has no success acknowledgement for a message send, so a denial (a missing message_send grant, a rate limit, an oversized payload) can only arrive later on the sendRejected event. Keep the .catch() for local and transport errors, and keep the sendRejected listener for everything the server refuses.
reply() and channel.send() are different. msg.reply() posts a true reply: it renders the quote header and fires a “replied to you” notification that pierces the author’s mutes. For a bot that is just answering, like this ping bot, msg.channel.send() is the right call. Use reply() when the quote header genuinely helps.
First message takes a moment
The first time a bot joins a server, another member who already holds that server’s key needs to be online to wrap it and hand it over. Until that lands, the bot connects fine and receives nothing it can read, and a send throws:If nothing happens
The bot connects but never sees a message
The bot connects but never sees a message
Delivery is view-gated by the server.
messageCreate fires only for channels the bot is permitted to view, so a silent channel can mean “not permitted to see it” rather than “nobody is talking”. Check client.visibleChannelIds(serverId) and confirm the view_text grant was approved on the consent screen.The bot replies to nothing and logs a send denial
The bot replies to nothing and logs a send denial
Read the
sendRejected payload. It carries the deny code, a message, and often the permission that was missing. Its serverId and channelId are attributed best-effort from your most recent send, so treat them as a hint.Login fails and the bot stops retrying
Login fails and the bot stops retrying
Two failures are terminal and stop the reconnect loop: code
-2, a revoked or regenerated token, and code -12, an SDK below the backend’s minimum wire version. Both emit disconnect rather than throwing. Other failures, including -8, are transient and keep retrying. See Connection lifecycle.The process will not start at all
The process will not start at all
Run the dependency report before anything else. It prints your Node version, whether it satisfies the SDK’s range, and whether a secure random source is reachable.Run it from a file, not with
node -e. See When a bot will not start.What just happened
- Your bot logged in and published its identity to Cloak, then wrote it to the keystore so the next run reuses it.
- It declared the permissions it needs, and the server pushed back the subset a human approved.
- It started receiving messages from every channel it is permitted to view. This is called the firehose, and it means you never subscribe to channels by hand.
- When it saw
!ping, it encryptedpongin your process and sent the ciphertext. Cloak relayed it and the sender’s app decrypted it.
Next steps
How bots work
The model behind that
pong.Permissions
Declare what your bot needs and check what it was granted.
Sending messages
Options, replies, mentions, and the send rules in full.
Deploying a bot
Put it on a host, and keep the keystore alive.