Skip to main content
By the end of this guide you will have a running bot that answers !ping with pong in any channel it can see.

Prerequisites

  • Node.js ^20.19.0 || >=22.12.0. Check with node --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.
If you lose the token you cannot recover it. Create a new one from the same screen. Regenerating a token invalidates the old one, and a bot still holding the old one stops retrying and disconnects.
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 bot.ts with the following. Read the token from an environment variable so it stays out of your code.
bot.ts
Add your keystore to .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:
The SDK retries the key pull automatically, so once a member comes online the bot catches up on its own.
Keep the person who added the bot online for a minute after the first join. That is usually all it takes for the key handoff to complete.

If nothing happens

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.
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.
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.
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 encrypted pong in 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.