Bots

You find it like a person.

A PrivChat bot is an automated account: behind it there is no person with the app, but a program someone —its operator— keeps running on a computer of their own, always on. Since there are no servers here, there is also no bot directory, no @names and no API on the Internet: you find a bot through its invitation, like a person. This page has two halves: the first is for anyone who uses the app; from «Setting up a bot» down, it is for operators and programmers.

A small computer running on a desk in the dark.
To use a bot you need none of this What a bot is here What a bot sees, and what it doesn't Setting up a bot Parameters Channels from the bot 1:1 contacts The bot as a home Mailbox Programming a bot: the local API Rules a bot must respect Common problems

01

To use a bot you need none of this

Using a bot is just using the app. If someone hands you a bot's invitation (a text or a QR, usually published on its website):

  • If it is a channel: New → Join a channel, paste the invitation text (it starts with UEND) or scan its QR, and Join. Inside, Refresh fetches the posts; the first poll can take a couple of minutes until the channel's address is published on Tor.
  • If it is a contact: New → New contact, paste the invitation or scan the bot's animated QR, give it a name and Add. Your first message carries the encrypted handshake; once it arrives, the bot has you as a contact and answers whatever its program says.
  • Verify it: inside the chat, More → Verify in person → Paste their code, with the code the operator publishes, signed, on their website.

And this is what a bot cannot do, by design:

  • It cannot read anything you do not send it yourself. Your conversations, your groups and your channels — it neither sees them nor knows they exist.
  • It cannot get in where it is not invited. It joins a group only if the admin introduces it, like any member; and it cannot publish to a channel that is not its own: each channel is written only by whoever holds its key.
  • It does not know who reads its channel. Readers fetch posts over anonymous circuits, never identifying themselves.
  • There are no «official» PrivChat bots. Every bot answers to its operator, who is who receives what you write to it. If who is behind it matters to you, verify it with its published code.

02

What a bot is here

  • Just another peer. It has its own identity, its own Tor onion service, its own encrypted database and its own channels. It is the app's engine compiled without Android.
  • Indistinguishable on the wire. Same envelopes, same encryption (Double Ratchet and PQXDH), same cover traffic, same channels. The app does not know it is talking to a program, because there is no way to know.
  • No directory. A central directory would be the server this app exists not to have. The operator hands out the bot's invitation wherever they like: a web page, a poster, a message.
  • Local API. Your program (Python, Go, shell, anything) talks to the bot over a Unix socket in its directory, in JSON. Tor takes care of the remote part.
  • Two uses, in this order. A channel: the bot publishes and anyone reads, anonymously. And 1:1: people write to it and the bot answers.

03

What a bot sees, and what it doesn't

About its channel readers, nothing. A reader fetches posts over an anonymous circuit without identifying itself; the bot sees that someone read, when and how much, and cannot see more because the protocol has nowhere to put it. A channel with a thousand readers knows exactly as much about them as one with ten.

About whoever writes to it, what any contact sees. Writing to a bot hands its operator your identifier, your return Tor address and what you write. With a human contact that is spread around; with a popular bot it is concentrated on one server. That is why channels come first, and why the bot keeps no history: an incoming message lives until the operator's program confirms it, with a seven-day cap.

If the bot's server falls into the wrong hands, they take the bot's identity (and with it the ability to impersonate it forever: there is no revocation), its channel keys and its contact list. They take nothing from any user or any reader. Recovery is destroying the bot, creating a new one and publishing its new fingerprint out of band.

Nothing tells you a contact is a bot, nor that it is the bot it claims to be. A serious operator publishes the bot's verification code, signed, on their website; in the app, inside the chat, More → Verify in person → Paste their code (or scan its QR) checks it just like with a person.

04

Setting up a bot

This part is for operators. You need a Linux of your own —a server or a 64-bit Raspberry Pi 4 or 5—, Java 17 or newer (sudo apt install openjdk-17-jre-headless), about 300 MB of free memory and a way out to Tor. A phone cannot be a bot: a bot has to be always on and always on the network, and that is a computer of the operator's, not a mobile.

The package: privchat-bot-2.19.2.zip (scripts, libraries and the native arti; Linux x86_64 — Windows for development only). The Raspberry (aarch64) package will be published once it has been tested on the real board, not before.

SHA-256 of the package:

18d98f258b9206dd2be9f8e89bd19b18db5fb55eee53ee8fc6e7745573f8f6fe
sudo unzip privchat-bot-2.19.2.zip -d /opt && sudo mv /opt/privchat-bot* /opt/privchat-bot
sudo useradd --system --home /var/lib/privchat-bot --shell /usr/sbin/nologin privchat-bot
sudo install -d -o privchat-bot -g privchat-bot -m 0700 /var/lib/privchat-bot

# Create the bot: seal, identity, prekeys, database. Shows the passphrase ONCE.
sudo -u privchat-bot /opt/privchat-bot/bin/privchat-bot --datos /var/lib/privchat-bot init --passphrase-generada

# The passphrase, in a credential only root can read (never in an environment variable)
sudo install -d -m 0700 /etc/privchat-bot
sudo install -m 0600 /dev/stdin /etc/privchat-bot/passphrase <<< 'the-passphrase-init-showed'

# The service
sudo cp /opt/privchat-bot/etc/privchat-bot.service /etc/systemd/system/
sudo systemctl enable --now privchat-bot

The passphrase is the whole defence against whoever copies the disk. A server has no phone Keystore, and pretending otherwise would be worse than saying so. Long or generated; with a TPM 2.0, bound to the machine with systemd-creds encrypt --with-key=tpm2 and LoadCredentialEncrypted= in the unit; without one, the 0600 file on an encrypted disk. The bot has no PIN and no screen lock: nobody is typing in front of it.

05

Parameters

optionwhat it doesdefault
--datos DIRthe bot's directory./privchat-bot
--credencial FILEpassphrase in a 0600 filesystemd's credential, or the console
--socket PATHthe API's Unix socket (short path: AF_UNIX allows ~100 bytes)DIR/api.sock
--api-tcp PORTadditionally, the API on 127.0.0.1 with a tokenoff
--api-grupo GROUPthat system group can open the socket too (0660)only the user (0600)
--retencion-dias Ndays an unconfirmed incoming message lives7 (max 30)
--max-contactos Ncap on contacts through public invitations1000
--sin-rellenoturns cover traffic offon, like the app
--buzonacts as its owner's Mailbox: the In person local network as well as Toroff
--bluetoothwith --buzon, also over Bluetooth LE (Linux only, BlueZ)off
--malla PORTLoRa mesh: a Meshtastic node by USB (/dev/ttyACM0, COM4) or TCP (host:4403); texts go out by radio when Tor cannot deliver (bot 2.16.1)off
--verbosodetailed logging, including Tor's. Debugging onlyoff

Without the daemon: init, passphrase cambiar, copia-seguridad DEST.zip (to restore on another machine, never to run two identical bots), restaurar SOURCE.zip and destruir --si. The log never contains onion addresses or full identifiers, and no served channel read leaves a line.

06

Channels from the bot

B="sudo -u privchat-bot /opt/privchat-bot/bin/privchat-bot --datos /var/lib/privchat-bot"
$B canal crear "Town notices" "Water cuts and festivals"          # returns canal_id
$B canal invitacion <canal_id> --png /var/www/notices.png         # the pasteable text, and the QR in the PNG
$B canal publicar <canal_id> "No water tomorrow from 9 to 12"
$B canal publicar-foto <canal_id> /path/poster.jpg "The poster"   # re-encoded: EXIF dies here
$B canal listar                                                   # posts, polls and bytes served
$B canal recortar <canal_id> 50                                   # keep only the last 50

Every post is signed by the channel key, encrypted with its reading key and chained to the previous one: history can stop being served, but it cannot be rewritten without readers noticing. 12 KB per text; a photo is re-encoded until it fits in 900 KB.

07

1:1 contacts

Two kinds of invitation, in the same format as the app's:

personalpublic (--publica)
who can use itone person: the first; the second is rejectedanyone, up to --max-contactos
one-time prekeysfive, like the appnone: shared among thousands they would not be one-time
what it costsnothing neweach user's first message to the bot lacks the prekey's extra protection against a future compromise of the bot's keys; the ratchet heals as soon as the bot replies
for whatan assistant for yourself, a notice for your familya bot open to the public
$B invitacion crear web --publica                        # the pasteable text
$B invitacion crear web --publica --gif /var/www/bot.gif  # plus the invitation's four QR codes in an animated GIF
$B verificacion codigo --png /var/www/verification.png   # publish it signed: that is how people know the bot is THE bot
$B mensaje escuchar --reponer                             # JSON events, one per line
$B mensaje enviar <peer_id> "Hello"
$B mensaje enviar <peer_id> "Hello" --retardo 3000       # goes out at a random moment within the next 3 s
$B mensaje confirmar <msg_id>                             # the bot forgets it

08

The bot as a home Mailbox

With --buzon, the bot acts as one person's Mailbox, its owner's: on a Raspberry Pi or a computer at home, it keeps and hands over their messages while they are out. Whoever comes by that Wi‑Fi leaves what they have for the owner and picks up what the owner left for them; and if the owner turns on "With internet" on their phone, their contacts can also leave messages over Tor while the phone is off. In the systemd unit, add --buzon to ExecStart, before run.

$B buzon invitar                        # paste the text on the phone: Settings → My Mailbox; the first to use it is the owner
$B buzon estado                         # the owner and what it keeps

Bluetooth (Linux only, optional). With --buzon --bluetooth the Mailbox can also be found over Bluetooth LE: whoever comes by the door with "Talk in person" on for the owner leaves and picks up messages without joining the Wi‑Fi. It needs BlueZ running, an adapter, the service user in the bluetooth group and, in /etc/bluetooth/main.conf, section [General], ReverseServiceDiscovery = false (then restart the bluetooth service): without it, the phone shows a pairing request and the channel does not start. That setting also turns off BlueZ's GATT client for other Bluetooth LE devices, so it is best on a dedicated device. It is off unless asked for, because the advertisement tells anyone scanning from the street that there is a PrivChat there, though not whose.

It only keeps the owner's messages, sealed, and the daily keys of the contacts the owner allows: never their identity. If it is stolen, it reads nothing and within about 48 hours it no longer recognises anyone; you unpair it from the phone. Its .onion address is fixed from the first start, even if it has never had internet. What it looks like on the phone: see the guide.

09

Programming a bot: the local API

One request or response per line, JSON, over the socket. Authentication is the file system: whoever can open the socket can read the bot's directory. Events arrive over the same connection after suscribir.

→ {"id": 1, "op": "estado"}
← {"id": 1, "ok": true, "peer_id": "…", "huella": "…", "onion": "….onion", "tor": "online", "contactos": 3, "canales": 1}
→ {"id": 2, "op": "canal.publicar", "canal_id": "…", "texto": "Good morning"}
← {"id": 2, "ok": true, "seq": 42}
← {"evento": "mensaje.recibido", "peer_id": "…", "msg_id": "…", "texto": "hello", "fecha": 1788500000}
operationparametersreturns
estado—identity, onion, Tor state, counters
suscribirreponereventos[] with what is unconfirmed; then live events
invitacion.crearetiqueta, publica, qrinvite_id, texto, qr_gif
contacto.listar / renombrar / borrarpeer_id…—
verificacion.codigo / comprobarpeer_id, codigotexto and QR / resultado
mensaje.enviarpeer_id, texto, respuesta_a, retardo_msmsg_id
adjunto.enviar / adjunto.leerpeer_id, nombre, base64 or ruta / msg_idmsg_id / bytes
mensaje.estado / mensaje.confirmarmsg_idstate / — (deleted)
canal.crear / invitacion / publicar / publicar_foto / recortar / borrarcanal_id, texto, pie…seq, texto and QR…

Events: mensaje.recibido, adjunto.recibido, contacto.nuevo, entregado, tor. They are at least once: a message is replayed on every suscribir until you confirm it, and the msg_id lets you deduplicate. Errors carry a stable code (sin_sesion, invitacion_invalida, demasiado_grande…); an internal error never carries details to the client. Operation and field names are in Spanish, the project's language: they are a stable contract, not prose.

A whole echo bot, in Python and without dependencies:

import json, socket
s = socket.socket(socket.AF_UNIX); s.connect("/var/lib/privchat-bot/api.sock")
f = s.makefile("rw", encoding="utf-8")
def call(**p):
    f.write(json.dumps(p) + "\n"); f.flush(); return json.loads(f.readline())
r = call(id=1, op="suscribir", reponer=True)
for ev in list(r["eventos"]) + [json.loads(l) for l in f]:
    if ev.get("evento") == "mensaje.recibido":
        call(id=2, op="mensaje.enviar", peer_id=ev["peer_id"], texto="echo: " + ev["texto"], respuesta_a=ev["msg_id"])
        call(id=3, op="mensaje.confirmar", msg_id=ev["msg_id"])

Two complete examples ship with the package: eco.py (which separates responses from the events that cross them, as you must as soon as a bot replies while listening) and rss_a_canal.py, which posts every new entry of an RSS or Atom feed to a channel.

10

Rules a bot must respect

  • Confirm what you process; what you don't confirm is replayed to you and deleted after seven days.
  • Don't fetch what you are linked to: a bot that can be made to request any URL is a bot someone will use for that.
  • Treat text as data: it comes from strangers.
  • Limits: 12 KB per channel text, 7.5 MB per attachment, 900 KB per channel photo, 16 MB per API line.
  • Scale: hundreds of 1:1 contacts, yes; thousands, through a channel.
  • An onion that never goes off is easier to trace back to its guard than a phone; that is why the bot runs with vanguards, and why it should not run next to things you would not want linked to it.

11

Common problems

symptomlikely causewhat to do
tor: arrancando for minutesno network, or an antivirus or proxy intercepting TLSturn the interception off; --verboso shows Tor's reason
"Unix domain path too long"the socket path exceeds ~100 bytes--socket /tmp/pb.sock
the first poll from the app brings nothingthe channel's address is not yet published on Torwait a minute or two and Refresh
a message stays "queued" for a whilethe rendezvous circuit takes timewait; the bot confirms as soon as it arrives
contacto listar does not show who just added the botthe bot learns about a contact from their first messagehave them write something
sin_sesion when sendingthat peer_id is not a contact yetcontacto listar