Mint a bot in the app
Open Qwing → Settings → My Bots → Create. Set a name, username, description and emoji avatar. You receive a 24-word recovery phrase, shown once — that phrase is the bot's token.
Run the SDK on your server
Plug that token into the Rust SDK, define how the bot replies, and run it. It publishes prekeys, holds the single-instance lock, and polls forever — no infrastructure of ours to depend on.
Overview
What a Qwing bot is
A bot is a normal Qwing account you control from code. It has its own ML-DSA-87 identity, its own ML-KEM-1024 ratchet, and it talks to users over the exact same post-quantum end-to-end-encrypted channel that people use. There is no bot API that sees plaintext: the server stores and forwards ciphertext, and your bot process does the decryption locally with keys derived from its token.
Create
Mint the bot identity
Bots are created from inside Qwing, not on a website — the private key never leaves your device until you copy it. In Settings → My Bots, accept the Bot Developer Agreement, then set the profile (name, username, description, single-emoji avatar). Qwing shows the bot's 24-word recovery phrase exactly once. Store it somewhere safe.
Quickstart
Run the SDK
The SDK is a small self-hostable Rust crate. Give it the token, register your handlers, and call run() — it connects, publishes prekeys, and polls forever.
use qwing_bot_sdk::BotClient;
#[tokio::main]
async fn main() -> qwing_bot_sdk::Result<()> {
let mut bot = BotClient::from_token(&std::env::var("BOT_TOKEN")?)
.connect().await?; // derives identity, publishes prekeys
bot.on_message(|msg, ctx| async move {
match msg.text.as_str() {
"/start" => ctx.reply("Hi! Send me anything and I'll echo it.").await,
other => ctx.reply(&format!("echo: {other}")).await,
}
});
bot.run().await // polls forever
}
Set the token (and, if needed, the API endpoint) in the environment:
# liboqs must be on the library path at build and run time
export BOT_TOKEN="word1 word2 … word24"
export BOT_BACKEND="https://api.quantumwing.io/api/v1"
cargo run --release
Handlers
Commands & replies
There is no fixed command set — you decide what the bot understands inside on_message by matching on msg.text. Commands are just messages that start with / by convention. The context object gives you the reply primitives:
- ctx.reply(text)Send a plain text reply to the user who messaged the bot.
- ctx.reply_with_buttons(text, rows)Reply with tappable inline buttons; each carries a callback payload.
- bot.on_button(cb)Handle a button tap —
cb.datais the payload you attached. - msg.textThe decrypted inbound text. Match it to route
/start,/help, or anything you define.
bot.on_message(|msg, ctx| async move {
match msg.text.as_str() {
"/start" => ctx.reply("Welcome. Try /menu").await,
"/menu" => ctx.reply_with_buttons("Pick one:",
&[&[("Weather", "pick:weather"), ("News", "pick:news")]]).await,
_ => ctx.reply("Unknown command — try /start").await,
}
});
bot.on_button(|cb, ctx| async move {
ctx.reply(&format!("You picked {}", cb.data)).await
});
Rules
One token, one instance
Run exactly one process per token. The bot holds a single-instance lock; two processes on the same token would fork the ratchet and permanently break decryption for the people it talks to. If you redeploy, stop the old instance first, and keep the bot's data directory with it — that is where its session state lives.
Privacy
What the server sees
The same as for any Qwing chat: nothing readable. Messages between a user and your bot are end-to-end encrypted with post-quantum cryptography; the server relays ciphertext and cannot read it. Your bot is the other end of that encryption, so it naturally sees the messages sent to it — and nothing else. It has no window into any other conversation.
Ready to build? Mint a bot in the app and point the SDK at its token.