Bots
A bot is an Aleja account that your code signs in as. It joins nests, reads messages, and replies, reacts, edits and deletes like a member, and it shows a BOT badge everywhere its name appears.
Bots are full end-to-end encrypted participants. A bot has its own identity keys, generated on the machine that runs it, and members encrypt to it the same way they encrypt to each other. The server never sees a bot's private keys and can't read what members send it or what it sends back.
To write one, see the bot SDK reference.
How encryption works for bots
Aleja encrypts every nest channel and direct message on members' devices. A bot takes part in that the same way a member's app does:
- Keys are made on the bot's host. When the bot first starts, the SDK generates an X25519 identity key, an Ed25519 signing key, a signed pre-key and a batch of one-time pre-keys. It keeps the private halves in a local state file and uploads only the public halves.
- The server stores public keys only. They go into the same pre-key bundle the apps publish,
through the same
PUT /v1/crypto/bundleendpoint. There's no separate bot key store, and nothing on the server can decrypt for the bot. - Adding a bot to a nest hands it keys. Each member's app sends the bot its current channel sender keys, each sealed to the bot over a pairwise Double Ratchet session. Members who are offline do the same when the bot first asks for their key.
- The bot decrypts locally. Messages arrive over the socket as ciphertext, and the SDK opens them on the bot's machine.
- Replies are encrypted locally too. The first time the bot posts in a channel, it creates its own sender key and seals it to every member. Direct messages use a pairwise session, as they do between people.
- Removing a bot rotates keys. When a bot leaves or is removed from a nest, the members who stay switch to new sender keys, so the bot can't read anything posted afterwards.
What the server can see is metadata, as it can for every member: which channels the bot is in, when messages are sent, how large they are, and who reacts to what.
Limitations
These follow from the design, and are intentional:
- No history. A bot can read messages sent after it joins and gets keys. It can't read anything posted before.
- Some early messages can be missed. If a member was offline when the bot was added, the first
message they send afterwards may be unreadable to the bot. The bot asks for their key when that
happens, and it can read everything they send after that. The SDK reports the missed message as an
undecryptableevent. - The state file is the bot's identity. If you lose it, the bot starts with new keys. Members' apps switch to the new keys on their own, but messages sent in between are lost. Back it up like a private key, and never commit it.
- One running copy per bot. Two processes sharing a state file will corrupt each other's ratchets. Run one process per bot.
- Bots can't start conversations with people. They can't send friend requests or open direct messages. A member can open a direct message with a bot, and the bot can answer it.
Creating a bot
Bots are registered in the developer portal, on the website. Settings → Bots in the app lists your bots and links there.
- Sign in to the console with your Aleja account.
- Click New application. Enter a username (3–32 characters: a–z, 0–9,
_or-), an optional display name, and the scopes the token may use. - Copy the token right away. It's shown once. If you lose it, reset it.
Each account can own up to 10 bots. In the console you can rename a bot, set its avatar and description, make it public, change its scopes, reset its token, set up OAuth2 installs and webhooks, edit its slash commands, take it out of nests, or delete it.
A public bot can be installed in a nest by anyone with Manage server there. A private bot (the default) can only be installed by its owner, in nests where the owner has Manage server.
Bot accounts can't sign in with a password, and the token can't be used to manage the bot. Only the owner, signed in to the portal, can rename, reset or delete it.
Adding a bot to a nest
You need the Manage server permission (or Administrator, or nest ownership). Either:
- open the bot's install link (see OAuth2 installs), pick the nest and approve the scopes it asks for, or
- open Nest settings → Apps → Integrations, enter the bot's username under Bots and click Add bot. That grants every scope the token has.
The bot joins with the @everyone role, so it sees the channels @everyone sees. Give it roles like any member to widen or narrow that. Remove it from the same list, or kick it like a member. Bots can't be added through invite links or Add member.
Tokens
A bot token looks like abot_ followed by 64 hex characters. The server stores only a keyed digest
of it (HMAC-SHA256 under a server secret kept outside the database), so a copy of the database can't
be used to check guesses.
- Send it as
Authorization: Bearer abot_...on HTTP requests. - The realtime socket takes it as
wss://<api-host>/v1/ws?token=abot_.... - Resetting the token disconnects the running bot, and the old token stops working right away.
- Deleting the bot revokes the token, removes the bot from every nest (with key rotation), and deletes its public keys. Its old messages stay, still marked BOT.
Treat a token like a password: anyone holding it can read and post wherever the bot is a member, though they still need the state file to decrypt anything.
What a bot token can reach
A bot token works only on the endpoints its scopes cover. Everything else
returns 403 Bots can't use this endpoint., and a listed endpoint whose scope the token lacks returns
403 naming the scope. In a nest, the scope must also be in what that nest granted the bot.
| Area | Endpoints | Scope |
|---|---|---|
| Profile | GET /v1/me | Always |
| Realtime | GET /v1/ws, GET /v1/bootstrap | gateway |
| Key exchange | GET/PUT /v1/crypto/bundle, GET /v1/crypto/bundle/{user_id}, GET /v1/crypto/identity/{user_id}, POST /v1/crypto/prekeys, GET/POST /v1/crypto/whispers, DELETE /v1/crypto/whispers/{id}, GET /v1/channels/{id}/crypto, POST /v1/channels/{id}/crypto/request | e2ee |
| Reading | GET /v1/channels/{id}/messages, GET /v1/channels/{id}/bootstrap, GET /v1/channels/{id}/pins, POST /v1/channels/{id}/read | messages.read |
| Writing | POST /v1/channels/{id}/messages, PATCH/DELETE /v1/messages/{id} | messages.write |
| Reactions and pins | POST /v1/messages/{id}/reactions, POST /v1/messages/{id}/pin | reactions, pins |
| Media | POST /v1/media, GET /v1/media/... | media |
| Nests | GET /v1/servers/{id}/members | members.read |
| Slash commands | GET/PUT /v1/bots/@me/commands | commands |
| Interactions | POST /v1/interactions/{id}/messages, PATCH /v1/interactions/{id}/messages/{message_id} | interactions |
Within those, the bot's nest roles apply as they would to a member: it can only post where it has Send Messages, pin where it has Manage Messages, and so on. Rate limits are the same as for members: 50 requests a second per token, and bursts of 5 messages refilling over 3 seconds.
Slash commands
A bot can register slash commands. When a member types / in a channel or direct message the bot
is in, its commands appear in the command menu under the bot's name, next to Aleja's built-in
commands and the apps.
Commands stay end-to-end encrypted:
- Definitions are public. The names, descriptions and options a bot registers are stored on the server in plain text, so every member's app can show them in the menu.
- What people type is encrypted. Running a command sends an ordinary encrypted message. Inside
it, next to a readable fallback like
/roll sides:20, is{ bot_id, name, options }. The server only sees ciphertext, the same as for any message. The bot decrypts it on its own machine, and the SDK reports it as acommandevent. Apps that don't know about commands show the fallback text. - Everyone in the channel sees it. A command is a message like any other: members see "used /roll with Dice Bot" and the options. The bot's answer is a normal reply.
A command reaches the bot only if the bot can see the channel and has Send Messages there. Members need Use Application Commands in a nest channel to see bot commands. It's on for everyone by default. In direct messages and groups, every bot in the conversation is offered.
The sender's app builds the invocation, so the SDK checks it against the bot's definitions before
emitting command. See the SDK reference.
A bot can take its commands over HTTP instead, as signed webhook events it answers through the REST API. That path is opt-in and not end-to-end encrypted: the app shows a "Not end-to-end encrypted" chip in the command menu and a NOT E2EE badge on the messages. See slash commands over HTTP and the Rust tutorial.
Command endpoints
These need a bot token. A member's session token gets 403.
| Method and path | Body | What it does |
|---|---|---|
GET /v1/bots/@me/commands | The bot's commands: { commands: [...] } | |
PUT /v1/bots/@me/commands | { commands: [...] } | Replaces all of the bot's commands. Returns { commands } as stored |
Members' apps load the commands for a channel from GET /v1/channels/{id}/commands (session
token). It returns { commands: [{ bot: { id, username, name, avatar_url }, name, description, options }] }. When a bot changes its commands, the server tells the members who share a nest or
conversation with it to refresh.
A command definition:
{
"name": "roll",
"description": "Rolls a die.",
"options": [
{
"name": "sides",
"description": "How many sides the die has",
"type": "integer",
"required": true,
"choices": [{ "name": "d6", "value": 6 }, { "name": "d20", "value": 20 }]
}
]
}
| Rule | Limit |
|---|---|
| Commands per bot | 100, with unique names |
| Command and option names | 1 to 32 characters: a–z, 0–9, _ or - |
| Descriptions | 1 to 100 characters |
| Options per command | 25, with unique names. Required options come before optional ones |
| Option types | string, integer, number, boolean, user |
| Choices | Up to 25 per string, integer or number option. Each has a name (1 to 100 characters) and a value of the option's type |
| Total size | 64 KB of JSON |
Invisible characters are removed from descriptions, and text that breaks the naming rules is
refused, the same as for display names. A 400 explains the first problem found.
Management API
Creating, updating and deleting bots happens through the developer portal's API, with a portal
session; see apps. With a member's session token, GET /v1/bots lists your bots
read-only: { bots: [...], limit, portal_url }.
keys_published on a bot object turns true once the bot has started and uploaded its public keys.
Until then, members can't encrypt to it.
Bots in a nest
These need Manage server in that nest. Removing a bot also accepts Kick Members.
| Method and path | Body | What it does |
|---|---|---|
GET /v1/servers/{id}/bots | Lists the nest's bots with owner and online state | |
POST /v1/servers/{id}/bots | { bot } (username or id) | Adds a bot, granting every scope its token has |
DELETE /v1/servers/{id}/bots/{bot_id} | Removes a bot and rotates the nest's keys |
Errors
| Status | When |
|---|---|
400 | Invalid username or display name, over the 10-bot limit, the account isn't a bot, or invalid command definitions |
401 | Missing, wrong or reset token |
403 | A bot token on an endpoint outside its list or without the scope, a session token on the command endpoints, adding someone else's private bot, or missing Manage server |
404 | No such bot, or it isn't yours |
409 | Username taken or reserved |
429 | Rate limited. Wait retry_after_ms (also in Retry-After) |
Spotting bots in the app
Bot accounts carry "bot": true on user, member and message objects, and the app shows a BOT
badge next to their name in chat and in the member list.
