Build your bot
A live message counter: a bot that sits in your chat rooms with its own key, counts every message, and every 5 seconds pushes the total to a public channel that every page shows. About ten minutes, and it runs on Pikoman today.
-
Make the bot a key
In the dashboard, open your app and choose New API key. Start from the Bot template and change it to this: the counter only listens in the rooms, so it does not need
client_events, and it may publish tostatusand nowhere else.Bots need Pro. A key that joins channels (
subscribe) is a bot's key, and the dashboard makes one on the Pro plan. Without Pro, the counter can still run as a backend script: the HTTP version below needs onlystatsandpublish.New API key, filled in for the counter bot dashboard.pikoman.se - Start from
- Chosen: Bot
- Name
- Counter bot
- Rights
-
Chosen:
subscribeChosen:publishNot chosen:client_eventsNot chosen:statsNot chosen:clientsNot chosen:disconnectNot chosen:disconnect_all - Channel kinds
- Chosen: public Chosen: presence Not chosen: private
- Channels
-
Chosen: Only some channels
exact name
status, prefixpresence-chat- - Addresses
- Chosen: Only these addressesthe server the bot runs on
- Expires
- Never, or in 90 days for a trial
Create API key shows the secret once:
sk_k_and 16 characters, a dot, and 43 more. It goes inPIKOMAN_API_KEYwhere the bot runs, and nowhere else: not in a page, not in git. Deleting the key in the dashboard stops the bot within about 30 seconds. -
Write the bot in Node
The library signs everything with the bot's key: a join token for each room (its header names the key, so Pikoman checks that key's rights and lists the bot as a bot), and a new token for each HTTP request, bound to that one request. Access 0 means it listens and cannot speak in the rooms.
terminal npm install @pikoman/ws@nextcounter-bot.mjs // counter-bot.mjs: counts the messages in your chat rooms and shows the total on every page. // It joins the rooms with its own API key, as a listener, and pushes the count to the public // channel "status" every 5 seconds. Node 22+, npm install @pikoman/ws@next // Named imports: Node loads the package as CommonJS, where the default import is the whole module. import { Pikoman, OfflineAuthenticator, Rest } from "@pikoman/ws"; const ROOMS = ["presence-chat-lobby", "presence-chat-help"]; // PIKOMAN_APP, and PIKOMAN_API_KEY: the bot's own key, sk_k_….…, never your backend's. const pikoman = new Pikoman(process.env.PIKOMAN_APP, { ws_address: process.env.PIKOMAN_WS ?? "wss://next.pikoman.se", }); const rest = new Rest({ address: process.env.PIKOMAN_HTTP }); // https://next-api.pikoman.se // Join tokens signed with the bot's key. Access 0: it listens, and never speaks in the rooms. const auth = OfflineAuthenticator.fromSecret(process.env.PIKOMAN_API_KEY, { name: "Counter" }, 0); let total = 0; for (const name of ROOMS) { const room = pikoman.listenPresence(name, auth); room.clientMessage(() => total++); // every client-… event a member sends room.refused((code) => console.error(`${name}: ${code}`)); // e.g. join_refused: check the key } // Every 5 seconds, changed or not, so a page opened a moment ago gets the number soon. setInterval(async () => { try { await rest.publish("status", "total-messages", { total }); } catch (e) { console.error("publish:", e.message); } }, 5_000);Run it PIKOMAN_APP=app-yourapp PIKOMAN_API_KEY=sk_k_… node counter-bot.mjsIt counts what is said while it runs; give it your own storage if the number must survive a restart. The bot is one connection of your plan, however many rooms it watches. It counts every client event, a bot's too: the live counter counts only the members' chat messages, in Node and Python, as a folder to download, and the milestones bot celebrates the 5 000th.
Or say it inside the room // The other way: say the total inside the rooms, as client-total-messages. // The key also needs client_events, and the bot joins with access 3 instead of 0. const auth = OfflineAuthenticator.fromSecret(process.env.PIKOMAN_API_KEY, { name: "Counter" }, 3); // In the loop over the rooms, after room.clientMessage(…): setInterval(() => room.send("total-messages", { total }), 5_000); // On the page, for members of the room: room.onClient("total-messages", (from, { total }) => room.isBot(from) && show(total)); -
Or over plain HTTP, in Python
No websocket and no library: the bot asks how many are in the lobby and pushes the number to
status, every 5 seconds. Each request carries a token signed with the bot's key and made for that request alone: the method, the path and a hash of the exact body. Its key: rightsstatsandpublish, the same channels as above.terminal pip install cryptography PIKOMAN_APP=app-yourapp PIKOMAN_API_KEY=sk_k_… python online-bot.pyonline-bot.py # online-bot.py: how many are in the chat room, on every page, every 5 seconds. Over HTTP only: # it asks /stats and pushes the number to the public channel "status". # Python 3.9+, pip install cryptography import base64, hashlib, json, os, time, urllib.request from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey APP = os.environ["PIKOMAN_APP"] API = os.environ.get("PIKOMAN_HTTP", "https://next-api.pikoman.se") # The bot's own key, sk_<key id>.<seed>: the id goes in each token, the seed signs it. KEY_ID, SEED = os.environ["PIKOMAN_API_KEY"].removeprefix("sk_").split(".") KEY = Ed25519PrivateKey.from_private_bytes(base64.urlsafe_b64decode(SEED + "=")) def b64(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode() def token(method: str, path: str, body: bytes) -> str: """An EdDSA JWT for this one request: method, path and a hash of the exact body.""" now = int(time.time()) header = {"alg": "EdDSA", "typ": "JWT", "kid": KEY_ID} claims = {"iss": APP, "iat": now, "exp": now + 60, "htm": method, "htu": path, "bh": b64(hashlib.sha256(body).digest())} signed = b64(json.dumps(header).encode()) + "." + b64(json.dumps(claims).encode()) return signed + "." + b64(KEY.sign(signed.encode())) def call(method: str, path: str, data=None) -> dict: body = b"" if data is None else json.dumps(data).encode() request = urllib.request.Request(API + path, data=body or None, method=method, headers={ "Authorization": "Bearer " + token(method, path, body), "Content-Type": "application/json", }) with urllib.request.urlopen(request, timeout=10) as response: return json.load(response) while True: try: online = call("GET", "/presence-chat-lobby/stats")["clients"] # bots count too call("POST", "/status/online", {"online": online}) except Exception as e: print("online bot:", e) time.sleep(5) -
Show it on your page
The page needs only your app id.
statusis a public channel: pages join it as listeners, with no token, and there is no member cap. Each open page is one connection of your plan.index.html <p>Messages so far: <strong id="total">–</strong></p> <p>In the lobby now: <strong id="online">–</strong></p>counter.js // On your page: the public channel "status", joined as a listener. No token, no key, no member cap. import { Pikoman } from "@pikoman/ws"; const pikoman = new Pikoman("app-yourapp"); const status = pikoman.listen("status"); status.on("total-messages", ({ total }) => { document.querySelector("#total").textContent = total.toLocaleString(); }); status.on("online", ({ online }) => { document.querySelector("#online").textContent = online.toLocaleString(); }); -
Next steps