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.

  1. 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 to status and 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 only stats and publish.

    New API key, filled in for the counter bot dashboard.pikoman.se
    Start from
    Chosen: Bot
    Name
    Counter bot
    Rights
    Chosen: subscribe Chosen: publish Not chosen: client_events Not chosen: stats Not chosen: clients Not chosen: disconnect Not chosen: disconnect_all
    Channel kinds
    Chosen: public Chosen: presence Not chosen: private
    Channels
    Chosen: Only some channels exact name status, prefix presence-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 in PIKOMAN_API_KEY where 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.

    Open the dashboard

  2. 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@next
    counter-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.mjs

    It 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));
  3. 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: rights stats and publish, the same channels as above.

    terminal
    pip install cryptography
    PIKOMAN_APP=app-yourapp PIKOMAN_API_KEY=sk_k_… python online-bot.py
    online-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)
  4. Show it on your page

    The page needs only your app id. status is 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();
    });
  5. Next steps

We are rebuilding the dashboard

Self-service sign-up is closed while we do. Until the new dashboard is ready we set up apps by hand: write to us with a line about what you are building, and we will set up your app and get back to you.

Write to hello@pikoman.se