Files
simplex-chat/bots
1b329f41fb core: wallet for names (#7684)
* core: wallet key for name ownership (CLI only)

A name has to be owned by an address the client can still derive after a
restart or on a new device. This adds that key and nothing else.

/wallet create makes one BIP-39 key per device and one BIP-44 account per
chat profile under it, /wallet shows the address that would own the next
name that profile buys, /wallet import and /wallet export move the key with
its recovery phrase.

A name key sits at m/44'/60'/<profile>'/0/<name>, which is ordinary BIP-44,
so the phrase reaches the same addresses in other wallets. No signing, so
nothing can be bought or edited yet.

Split out of #7390 / #7425.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: pin simplexmq with eth crypto merged into master

ab/eth-crypto forked before Crypto.Entitlement, which Chat.Badges imports,
so the branch had to be merged with simplexmq master before it could be
pinned here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: wallet review fixes, and show the derived addresses

Concurrency: the account index is incremented in SQL and read back in the
same transaction, so two profiles cannot be handed the same key. Importing
a phrase is one transaction and single_seed is UNIQUE, so a phrase cannot
be discarded in favour of a key created meanwhile, and a device cannot end
up with two keys.

Wallet commands are no longer forwarded to a remote host: the recovery
phrase must not leave the device, and the raw command is logged there.

/wallet delete removes the key, confirmed by the last word of the phrase,
so creating a key before importing your own is no longer a dead end.

/wallet now shows every profile on the key with the first two name
addresses each, to check derivation against other wallets. Hidden profiles
are left out, as they are by /users. A bad phrase no longer says which word
was wrong. /wallet export uses the profile's own key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: make the wallet an API, not a user-facing feature

Names commands will use the wallet; users do not. The commands move to
/_wallet, and the help section, its topic and the changelog entry are
removed. The tests drive the API end to end.

Comments cut to what the code does not say, dead exports removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: wallet export and delete act on the device key

The key is one per device, so both commands read it directly instead of
going through the active profile's account. A profile without an account
of its own can now export, and the delete error says that the key goes for
every profile on the device. The confirmation word is lowercased, as the
phrase is when imported.

Two known limitations are written down: account indexes restart at 0 after
an import, so profiles are bound in the order they ask for a key, and the
gap left by a hidden profile in the printed indexes shows that it exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: /_wallet delete takes no confirmation word

The wallet is an API, and no other API command takes a typed confirmation:
/_delete user takes the profile password. The word was also no barrier to a
caller, which can read it from /_wallet export. Confirming belongs in the
command that will wrap this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: name the unique index on wallet_seeds

The one key per device rule is meant to be lifted later. SQLite cannot drop
a column with an inline UNIQUE, or its automatic index, so lifting it would
have meant rebuilding the table. As a named index it is a DROP INDEX and a
DROP COLUMN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: export the secret of a single name key

/_wallet export gives the seed mnemonic, /_wallet export <account> <name>
gives the secret of one derived key, so a single name can be handed over
without the seed. Both are what a wallet takes on import: the mnemonic as a
recovery phrase, the secret as hex.

The commands are named for what they return, APIWalletExportSeedMnemonic
and APIWalletExportDerivedSecret, as the difference is which secret leaves
the device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: reject a key index BIP-32 cannot harden

A.decimal wraps into Word32 and hardened leaves anything at or above 2^31
alone, so /_wallet export 4294967296 0 returned account 0's secret under the
path m/44'/60'/0'/0/0. Handing out a different account's key for a mistyped
index is the wrong failure for this command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: /_wallet bind, and name the other profiles instead of numbering them

Restoring a chat database older than the key loses which account each
profile had, and the keys are still there, so /_wallet bind <account> sets
it by hand. The counter moves past an account bound this way, and an
account another profile holds is refused.

/_wallet now shows the paths of the active profile only, and names the
other profiles on the key. Their account indexes were the gap that showed
a hidden profile exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: say same seed, not same key, for the other profiles

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: create makes the seed, bind claims a path

The two were one command: on a device that already had a seed, /_wallet
create only bound a profile. Now create makes the seed and refuses when one
exists, and binds every profile that exists, as a new seed has no account
that already owns a name.

/_wallet import binds nothing. Which account a profile had is what an
import is recovering, and the seed does not say, so the profile says it
with /_wallet bind <account>. /_wallet bind with no account takes the next
free one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: the known issue is now about bind, not import

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: create binds no profile either

Create and import differed only in whether they bound a profile, which made
the convenient one inconsistent. Neither binds now, so the store keeps one
function for both, and a profile reaches an account only through bind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: test the restore cases the key has to survive

A database backed up after the key carries the key and the binding, so the
restore is all of it and an import is refused. A database backed up before
the key comes back with the profiles and no binding, so the phrase is
imported and each profile says which account was its. A key imported before
the database is restored is replaced by what the backup held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: drop the types nothing reads, and say account where it means account

AccountRef and WalletAccount were built and never read: deriveNameKey now
returns the key, and the address comes from addressFromPrivateKey, which
already exists. SeedId is a plain Int64 like every other row id here.

The master key is derived once per command rather than once per name key,
which is where PBKDF2 runs.

A device has a key and a profile has an account, so /_wallet says "no
account for this profile" rather than repeating "no wallet key", which is
what it says when the device has none.

Also: no "wallet: " in front of "bad chat command: ", strEncode for the
address, the repeated seed lookup in one place, comments cut to what the
code does not say, and the export test pins its addresses and secrets,
including one whose first byte is zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: store the seed as bytes, and bind one account per profile

On Postgres a bare ByteString binds as a text literal, so entropy with a
backslash or a high bit was rejected and entropy with a zero byte was
truncated: the BIP-39 test vector stored as an empty seed. Blob columns go
through DB.Binary here, as every other one does.

/_wallet bind with no account moved a profile that already had one to a
fresh account, abandoning the old one without saying so. It refuses now;
moving is asked for by number.

The counter could walk past 2^31, where BIP-32 hardening folds every index
back onto a low one, so a profile handed 2147483648 derived account 0's
keys and printed account 0's path. Binding refuses once the counter is
there, and a very long index is now rejected on its digit count rather than
after reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: rfc for the keys that own names

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: pin simplexmq for the BIP-44 path, and use it

ethereumPath now takes the address index, so the path a name key sits at is
built once in the library rather than again here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: refuse an account index BIP-32 cannot harden, whichever way it arrives

The counter path checked it, the explicit one did not, and the parser is not
the only caller: processChatCommand takes APIWalletBind from library callers
too. hardened leaves an index at or above 2^31 alone, so account i and
i + 2^31 are the same key while the duplicate check compares the stored
integers, and two profiles could hold one account's names.

The rfc now says what hiding a profile from /_wallet does and does not
protect: the seed is one per device, so whoever unlocks any profile can
derive every account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: pin simplexmq with the wordlist embedded from the upstream file

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: cut the rfc to what stands on its own

Drops the references to other documents and to what is or is not in a pull
request, so the design reads without them, and the list of tests, which
restated their names and would drift. 164 lines to 134, against a median of
117 for the rfcs here that define schema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: what the stealth meta address is for

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: the wallet is not only for names

The seed, the accounts and the derivation are a wallet. Owning a name is the
first thing it is used for, not what it is, and the title and the problem it
solves now say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: the reason for a key per name is that owners are public

One address owning several names links them: whoever knows one name can read
its owner and find the others. That is the reason, not the narrower export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: give the reason for the counter before what it costs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: names are the device's, so drop the per-profile account

A name's profile is the record it resolves to, not the key that owns it, so
the account level was carrying a mapping nothing needs. It was also the only
source of the profile to account ambiguity after a restore, of /_wallet
bind, and of the index gap and the refusal that told a visible profile an
account was held by one it cannot see. All of it goes.

Names sit in account 0 from index 1. Index 0 is left unused so neither the
names nor the profile accounts, which start at 1, claim the origin of both
dimensions. users gains no columns at all now, so the migration is one
CREATE TABLE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: rfc for names counted per device

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: one command for the seed, whichever way it arrives

create and import differed only in where the entropy came from and already
shared one store function, so they are one command with the source named:
/_wallet create new, or /_wallet create seed=<phrase>. Naming it means no
seed is generated by typing a prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: the argument is a mnemonic, so name it one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: the column holds entropy, so name it entropy

The mnemonic comes back from the entropy exactly, which is what export
returns, and the seed PBKDF2 derives from the mnemonic is computed when a
key is needed and never stored. The column was the one thing called seed
that is not the seed, which is why it needed a comment saying so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: 24 words is a choice, not a default

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: deleting the seed reports ok, not the empty wallet

Delete re-ran /_wallet, so a successful delete printed "no wallet key",
which is the same words the error uses and reads as a failure. There is
nothing to show after a delete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* docs: the export argument is an index, not a name

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: export says which kind of secret, so other kinds can follow

/_wallet export name <k> rather than an index straight after export. The
bare form stays the seed mnemonic, and a profile's key can be added beside
the name one without the argument having to be guessed from its shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* disencourage hidden profiles using names

* let remote use wallet, but filter logs to avoid leaking secrets

* change RFC wallet architecture to BIP-85 per profile and hardened paths for names

* introduce keyring as the thing a profile binds to

* restructure RFC

* shorten

* drop BIP85 and flattenhierarche. no keyrings

* /goal iteration 4 - unconfirmed

* /goal iteration 5 - unconfirmed

* repin

* rename to adapt

* cut comments

* next review round

* minimize diff and align style even more

* missing type mappings

* fix review comments

* self-review iteration 2

* self-review iteration 3

* self-review iteration 5

* iteration 6

* self-review iteration 9

* self-review iteration 10

* repin

* rng preserve

* refactor WalletSeed type, see mq wip

* repin

* infallibel wallet and account derivation

* review iterations

* repin to mq, derivation fallible again

* repin

* use deriveAddress, update simplexmq

* simplexmq

* add down migration for db and drop redundant masterkey field in db

* repin to simplexmq PR

* fable review fixes

* iterations on /goal

* treat hidden profiles same as normal ones. they can hold accounts now

* reduce explicit types

* repin mq

* doc cosmetics

* simplify

* query plans

* query plan

* simplify

* query plans

* simplify more

* fixes for comment ^^

---------

Co-authored-by: Alain Brenzikofer <alain@encointer.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-10-13 18:22:55 +01:00
..
2026-10-13 18:22:55 +01:00
2026-10-13 18:22:55 +01:00

SimpleX Chat bot API

Why create a bot

You can implement SimpleX Chat for these and many other scenarios:

  • customer support - both as a single- and a multi-agent support chat (using SimpleX Chat business address feature),
  • information search and retrieval bots, with or without LLM integration,
  • moderation bots, to moderate your group and communities.
  • broadcast bot, when messages from your trusted users are forwarded to all connected contacts - e.g., see our SimpleX Status bot in the app (source code),
  • feedback bot, when messages from connected contacts are forwarded to a preset list of your trusted users,
  • P2P trading bots, connecting buyers and sellers,
  • etc.

We will share all useful bots you create in the bottom of this page - please submit a PR to add it.

What is SimpleX bot

SimpleX bot is a participant of SimpleX network. Theoretically, bot can do everything that a usual SimpleX Chat user can do – send and receive messages and files, connect to addresses and join groups, etc. But to be useful, a bot should distinguish itself as a bot, and to provide an interface for the users to interact with it.

How to configure bot profile

Starting from v6.4.3, SimpleX Chat apps support bot configuration to distinguish bots, to highlight commands in messages, and to show command menus.

Set up bot profile

To distinguish SimpleX user profile as a bot, set its peerType property to "bot". It can be done in one of these ways:

  • using CLI options --create-bot-display-name and create-bot-allow-files when first starting CLI to create bot profile,
  • using command /create bot [files=on] <name>[ <bio>] (if name contains spaces, it must be in single quotes), when creating additional bot profiles in the same database,
  • by configuring bot commands that the users will see in the UI when they type / character or tap // button with /set bot commands ... CLI command (see syntax below),
  • by using APIUpdateProfile bot command to set peerType and configure bot commands at the same time.

Configure bot commands

Bot commands are messages that start from / character. Normally, they would consist of lowercase latin letters, but commands can use any characters, including symbols such as /+ or /-. Commands can have parameters.

All commands in messages will be highlighted in the chats with the bot, and when users tap them, they will be instantly sent. If the message has a single line and starts from / character, the whole message will be highlighted. Otherwise, if command is included as part of the message, it will be highlighted until the first space after / character: e.g., /list command in Directory service shows user's groups.

Please note: commands in messages will be highlighted based purely on / character, regardless of whether they are supported by the bot or included in bot configuration. It allows bots to have "hidden" commands that bot would support, but that won't be shown in the menu. But it may also lead to mistakes if bot sends incorrect commands in the instructions to the users.

Bots can also send highlighted commands with parameters. To do that, bots should surround both command and its parameters in single quotes: e.g., /'role 2'. Quotes won't show in the apps UI, and if the user taps this command, it will be sent as /role 2.

Configured bot commands will be be offered to the users as a menu, and for quick lookup as the user types.

Bot commands configuration is a property in preferences object in bot profile received by the user. These preferences can be configured both on the bot user profile level, to offer the same commands to all connected users, and as overrides for specific contacts, to offer different commands to different bot contacts.

Configuring commands in bot user can be done either with APIUpdateProfile or with /set bot commands CLI command:

/set bot commands <commands>

where:

commands = <commandOrMenu>[,<commandOrMenu>...]
commandOrMenu = command | menu
command = '<label>':/'<keyword>[ <params>]'
menu = '<label>':{<commands>}

This syntax allows creating nested menus of commands with and without parameters. You must enclose parameter names with the characters < and >. Currently, users have to edit command templates to set actual parameters, but in the future there will be UI support to fill in parameters based on this syntax. For example, some of SimpleX Directory service commands could be configured with this command:

/set bot commands 'How to use bot':/help,'Show your groups':/list,'Your group settings':{'Set default role':/'role <ID>','Set anti-spam filter':/'filter <ID>'}

Configuring commands for specific contacts can be done with APISetContactPrefs command.

Bots and business addresses

A useful scenario would be when bot is used to accept requests to bot's business address, so that a new business chat is created for every connecting user, and then invites other people from the business, as appropriate.

Business chat is a special group chat under the hood, but the connected customer sees business avatar, and all users that bot would add to the group later see customer' avatar. This chat will inherits preferences from the bot profile, so if you want to allow customers to send files, you need to allow them in the bot. And if bot has any commands configured, they will also be available to customers in the menu.

How to create a bot

SimpleX Chat CLI can be run as a local WebSockets server on any port:

simplex-chat -p 5225

To see all supported parameters:

simplex-chat -h

Your bot must run as a standalone process connecting to CLI via WebSockets on the chosen port. See Security considerations about connecting your bot process to CLI.

All communication between your bot process and CLI happens via JSON-encoded WebSocket text messages.

To connect to other SimpleX Chat users and to send messages the bot must send commands to CLI. The command WebSocket message contains correlation ID and commands as strings.

CLI will respond to command messages with command processing results. The response WebSocket message contains the same correlation ID as was sent in the command and JSON-encoded response record.

See Sending commands about message formats and types for commands and responses.

CLI will also send chat events to your bot process. These events represent information about connecting SimpleX Chat users, received messages, etc.

See Processing events about event message format and types.

In most cases, the bot needs to have a pre-configured user profile and SimpleX address, configured to automatically accept incoming contact requests from all users. It is simpler to do it manually via desktop client and then use this chat database with your bot. But it can also be done programmatically when bot starts.

In the simplest case, your bot must process NewChatItems event to receive messages from connected users and use APISendMessages command to respond to them.

Sending commands

CLI WebSockets API allows to:

  • send and receive messages and files.
  • create and change user profile - you also can do it manually, via SimpleX Chat desktop app or CLI.
  • create and accept invitations or connect with the contacts.
  • create and manage long-term user address, accepting connection requests automatically or via code.
  • create, join and manage group.

Each command your bot sends to CLI should have this JSON format:

{
  "corrId": "<any unique string>",
  "cmd": "<command string>"
}

You can use sequential numbers, UUIDs or some other unique strings in corrId field.

Command strings are the same commands you can can see in Settings / Developer tools / Chat Console of mobile and desktop apps. You can test these commands via SimpleX Chat CLI.

When command is processed, CLI will send a response as a WebSockets message in this format:

{
  "corrId": "<corrId sent with a command>",
  "resp": {
    "type": "<response record tag>",
    "other response fields": null
  }
}

corrId will be the same as you used in commands. Your bot must maintain the map of pending commands responses, and can implement an internal callback or async API for convenience. See our TypeScript bot library for an example. TypeScript library sends commands sequentially, via a queue, but your bot can send commands concurrently.

resp field is a command-specific response in JSON format. All command responses form a discriminated union with type field as a tag.

See API Commands and Responses reference about specific command strings and JSON types for command responses. As CLI has the same API as used by mobile and desktop apps, it supports other commands not included in the reference.

Please note: CLI uses network connection for most API commands. Command network usage is included in the reference:

  • "no" - command doesn't use network,
  • "interactive" - all or some network requests will complete before command response is sent to the bot,
  • "background" - command response will be sent to the bot before scheduled network requests are sent.

Processing events

Chat event is a WebSocket message in this format:

{
 "resp": {
    "type": "<event record tag>",
    "other event fields": null
  }
}

While it uses the same resp property as responses for backward compatibility, the event type is a different discriminated union. Some record types are used both as a command response and an event. The most important example is NewChatItems that can be sent both as a response to APISendMessages with correlation ID (when message is scheduled for delivery) and as events when messages are received.

See API Events reference about specific JSON types for chat events. CLI can send other events not included in the reference.

Please note: Your bot must allow and ignore all events it does not process, it should not fail when it encounters undocumented event types. Your bot JSON parser must allow additional properties in all types, and must allow and ignore records with unknown union tags and unknown enum strings.

Security considerations

WebSockets API of SimpleX Chat CLI does not support any authentication. CLI binds only to localhost to prevent accidental access from public network, in case you did not close this port in firewall. The messages in WebSocket API are not encrypted in any way, and must not be sent via public networks.

It is usually simpler to run your bot process on the same machine where you run SimpleX Chat CLI, and to close CLI port in firewall. That makes connection between your bot and CLI secure. It also simplifies sending and receiving files via bot, as they are stored on the file system accessible to SimpleX Chat CLI.

If you have to run your bot on another machine, you need to secure access to bot CLI via any web proxy that supports WebSockets, e.g. Caddy or Nginx. You must configure TLS termination in the proxy and connect CLI process from bot via a secure TLS connection. If you connect to bot via a public network, you also must configure HTTP basic auth to prevent unauthorized access. You can validate TLS security of your proxy via a free test at SSLLabs.com. You can also configure firewall on the machine where you run SimpleX CLI to only allow connections from the IP address of your bot.

Available libraries

Libraries with full bot API support

Useful bots