Bots off the box

Three ways to run klotto's ticket-buying and raising as their own process — and the one that turns buying a lottery ticket into a deal a KEA notary signs.

Scoping note · 17 Sep 2026 · testnet

The short version. Option A is an afternoon and gets the bots off the backend. Option C is a day or two, needs no new trust story, and is the only one that exercises the thing you actually want exercised — a stranger raising a transaction and a notary deciding whether to sign it. I'd do A now and C next, and skip B.

Update · 18 Sep

Option C is set up and tested offline (KLOTTO-TICKETDEAL1). klotto now publishes a fifth deal per pot, testnet-<cc>-ticket.v2 — a v2 policy for buying one ticket: { player, stake, ref }, the pot must be open, the stake in range. The bots raise it unsigned with KLOTTO_TICKETS=deal; a player-side notary signs it with kea/config/klotto.player.testnet.json and the player's own hourly budget. The offline proof is qa/klotto/ticket-tests.js in hbarlive_hts. The converter covers the v1 → v2 half of the queue. Still to do by hand: put a player key on a device and run kea go against that config — the live test needs the key, which is yours to place.

01Where it runs today

Everything lives in one pm2 fleet on the GCP VM, declared in ecosystem.kbot.config.js. The klotto pieces are two of about ten apps in that file:

pm2 appscriptdoesneeds keys
katdemo-klotto-pilotkbot/klotto/klotto-bots.jsBuys tickets as alice/bob/carol; raises claim and unlock when the chain says one is dueYes — the player accounts
katdemo-kea-pilotkea/node.jsThe box's own notary signerYes — the house keys

Two consequences worth naming before choosing anything. First, the raiser and a signer sit on the same machine as the backend and the pool, so one box compromise takes the whole loop — fine on testnet, not a mainnet shape. Second, pm2 resurrect brings both back on reboot, which is why the box signer reappeared after you stopped it.

02What the bot actually does

It is two unrelated jobs wearing one name, and that is the seam to cut along.

Job one — buyingA plain token transfer. Player pays the pot. Signed locally by the player's key. Nothing to do with deals.
Job two — raisingPOST to /katomia/deal-bytes with a deal template and its bindings, kpool:true. Holds no key. Signs nothing.

The raiser is already key-less: it hands the backend a template and parameters, the backend builds the bytes and pools them, and notaries elsewhere decide. That is why moving it is cheap — it is a client, not an authority.

03Three options

A. Lift and shift do this now

Same file, its own process, its own config, run anywhere with outbound HTTPS. The work is removing its two remaining dependencies on being inside the repo on the box:

Then it is klotto.config.json beside a checkout, gitignored, exactly like kea.config.json. Runs on a laptop, a phone, a Pi.

Cost: an afternoon. Buys: the demo stops depending on the backend box; you can run the players from anywhere and watch the notaries react.

B. Split buyer from raiser skip

Two processes: one holds player keys and buys, one holds nothing and raises. Clean on paper, and it is what you would do at scale.

But the split already exists inside the file, the two jobs share the pot state they both read from the mirror, and separating them now means two configs, two deploys and a new coordination question for no behaviour you cannot already get from A. Do it when there is a second application raising, not before.

C. Make buying a ticket a deal the interesting one

This is the exercise you described, and it is worth doing for its own sake.

Today a ticket purchase is a transfer the player signs itself. Instead: publish a ticket deal — a v2 policy that says "this account may move exactly the ticket price from a player account to the pot, provided the pot is open and the amount is one of the allowed stakes" — and have the buyer raise it unsigned, like any other deal. A KEA notary holding the player key then decides and signs.

What that gets you that nothing else does:

Cost: a day or two — most of it writing and pinning the ticket policy, which is the same shape as the existing claim policy but simpler (no timelock, no NFT, no digit match).

04The snags, honestly

Check first

Ticket volume vs. the pool's window. The pool keeps transactions about an hour and the dashboard reads a live window. Players buying every few seconds through the pool would swamp both. Option C probably wants a slower arrival rate, or a shorter valid window so tickets clear quickly.

Decide

Who holds the player keys under C? If the player's notary runs beside the buyer, you have moved the key, not removed it. The honest version has the buyer holding nothing and a separate notary — possibly on a phone — holding the player key. That is more moving parts, and it is also the whole point.

Live now

The BR claims are expiring unsigned, and it is a config mismatch, not the bots. The raiser declares c9ce3a30… — testnet-BR-claim, the v1 document. Your phones' kea.config.json lists 4b510865… — testnet-BR-claim.v2. Different deal, so the notaries correctly ignore it: zero verdicts, zero signatures, expired. Watch both ids, or swap to the v1 one the bot actually raises.

05What I'd do, in order

  1. Fix the BR deal idOne line per phone. The demo starts working again immediately.
  2. Option A — the bots get their own config and addressMirrors what kea.config.json already does. No new concepts.
  3. Write the ticket policyA v2 deal for "buy one ticket". Publish it in klotto's catalogue beside the claim and unlock.
  4. Option C — a player-side notaryOne more KEA instance, a player key, a tight per-hour limit. The first non-house signer.

06The queue behind this

Three things are waiting on roughly the same groundwork — a demo that runs as its own process, off the backend box, against a published deal.

KBAR rewards

The reward bot is already in the same pm2 fleet (katdemo-reward-pilot, kbot/reward-bot.js) and is in exactly the position klotto's bots are in: works, lives on the box, has no config of its own. It is the natural second customer for whatever Option A produces — same extraction, same shape, and it would prove the pattern generalises rather than being a klotto special.

xproof.live

Further out and larger. The zone is active in the Cloudflare account and unattached — so unlike xbar it needs a Worker pointed at it, not a zone move. The blocker is not hosting: the register has /domain/query down as needing a DNS-verified rebuild, and tx, kwt and the monitor all depend on xproof identity and cannot be re-enabled until it answers. That is a build, not an extraction, and it should follow the two above.

And the thing none of these fix

A notary on a phone in Termux is not a durable host — your own console shows the doorbell dropping and reconnecting every two minutes or so. That belongs in its own note, and it is the companion to this one.