Skip to content

Button

A green button on a pole. Press it, get coins, buy something that presses better.

It is deliberately a clicker: the loop is legible in three seconds, and it exercises every primitive in the SDK without anybody having to invent a reason. Play it, or read the source.

Local Button game showing the green button, reward counter, NPC shop board, shopkeeper, and targets

Local Button app at localhost:7777. The center board exposes press rewards and clearing; the left board is the implemented shop.

Capture provenance — illustration, not runtime evidence

The revision this was captured from was never recorded, so the state on screen cannot be re-created. Under the screenshot evidence contract that makes it an illustration of the running app and not proof of anything. What proves these pages is the source they embed and the commands printed beside it.

FieldRecorded
Filedocs/public/img/docs/button-gameplay.png, 1600 × 900 PNG
Integritysha256sum docs/public/img/docs/button-gameplay.pngc6d0eb231cf02b1e0329fbb8e83f9d324fb4e80c380212208fc9a4f38807123d
Repositorykeicoin-org/button
Revision at captureNot recorded. The capture cannot be reproduced.
Command or URLbun run dev, then http://localhost:7777
Network or mock modeMock. Button's dev server serves an in-memory node at /rpc from the same process; nothing on it survives the process.
ViewportNot recorded. The stored image is 1600 × 900; the browser and device pixel ratio it was taken at are unknown.
Scenario stateNot recorded. The issuer seed is generated per run unless KEI_GAME_SEED is set, so the asset ids and balances behind this screen differ on every start.
Alt text"Local Button game showing the green button, reward counter, NPC shop board, shopkeeper, and targets"
Added to this site4ed6462, 3 August 2026
Last reviewed5 August 2026
Stale-proof ownerThe keicoin-org/keicoin-site maintainers. Replacing it means capturing again from a pinned revision and filling every row above; a row that stays Not recorded keeps the image an illustration.

For the money loop on its own — press, bank, commit, claim, as a script you can run — start with Button fundamentals.

ClientBabylon.js, one bundle, no framework
ServerOne Bun process — the mock node, the game API, and the static client
DatabaseNone. No users, no balances, no inventory, no save file
ChainIn-memory mock, served over HTTP by the same process
Every line of Kei in the clientsrc/economy.ts, about two hundred lines including comments

Run it

sh
git clone https://github.com/keicoin-org/button
cd button
bun install
bun run dev          # http://localhost:7777

The server generates an issuer seed per run unless KEI_GAME_SEED is set. A new issuer means new asset ids, which is fine here because the ledger is new too.

The loop

PressClick the button, or hit space. Presses accumulate unbanked.
BankEvery 20 presses (or 3 seconds) the client asks the server to pay for them. The server adds you to the next batch.
ClaimThe batch becomes one commit block; you get a proof and your own wallet writes the claim.
BuyClick a row on the shop board. You transfer coins; the shopkeeper mints the item and burns the coins.
ExchangeOptional. Pay Kei, get coins at the posted rate. Turn it off and the game is unchanged.

Every primitive, and where it shows up

PrimitiveWhere
token.issueThe game creates its coin on startup, idempotently, in server/game.ts
items.createOne asset per upgrade archetype, with a real supply cap
commit / claims.addBanked presses become one issuer block; your wallet writes the claim
transferYou pay the shopkeeper in coins, signed by you
items.mintThe shopkeeper delivers the upgrade — only once the chain says the coins landed
burnThe coins you spent are destroyed rather than pooled
balanceOfThe screen on the pole, and the price check in the shop
wallet.summaryRestores every upgrade you own on page load; this is the save file
pay / onPaymentThe optional exchange desk: Kei in, coins out
faucetTops the player up on a non-mainnet network so the desk is usable

Why banking instead of minting

Minting per press would put every player's reward on the issuer's chain, and one account has one chain — so the issuer becomes a global write lock and the queue behind it becomes the game.

Presses are batched instead. Every player who banked in the same window ends up in one issuer block, and each of them then writes their own claim, from their own account, in parallel, with no contention.

ts
// server/game.ts — the issuer, once per window, however many players banked
const drop = await this.coins.commit(batch.map(([to, amount]) => ({ to, amount })))
for (const [address] of batch) {
  const bundle = drop.proofFor(address)   // plain JSON; hand it over however you like
  // ...resolve the waiting HTTP request with it
}
ts
// src/economy.ts — the player. From here the game is not involved.
// A session (below) already proved the address; every press already told the
// server it happened via /game/press. Banking asks for the payout, not a count.
const body = await post('/game/bank', { session, batch: batchId })
await kei.claims.add(body.bundle)

The address is never in that body. A session proves who is asking once — sign a challenge from POST /game/session/challenge, redeem it at POST /game/session — and every following call, including this one, carries the session id and nothing else. See Player rewards for the full session and observation protocol.

The batcher merges per address on purpose: a root commits to at most one entitlement per account, so two banks inside one window are one leaf, not two.

With one player it is a batch of one and the code is identical. That is the property that matters — this does not need rewriting when there are a thousand. See the batch rewards reference.

Why buying takes two signatures

The game cannot sign for a player's wallet, so a purchase is always the player signing a transfer and the issuer signing a delivery. A transfer carries no memo, so the shop takes the order first and matches the arrival to it — and delivers nothing until the chain says the coins landed.

ts
// src/economy.ts
const order = await post('/game/order', { session, sku })
await coins.transfer(order.to, order.price)
// The shop signs the delivery. There is no third arrangement in which one of
// them signs for the other.

The order is not the purchase. The order records intent; the arriving transfer is the fact. The server reconciles the two, and an unpaid order delivers nothing.

For the whole shopkeeper protocol — the quote, the verification, the mint, the burn, and what is still missing — see the NPC shop guide.

What a player is allowed to spend

The balance on the pole is the chain's figure and nothing is added to it. What a press has earned and not been paid for is a second, amber number beside it — presses still unbanked, plus whatever is in flight — and it is never part of the balance.

That split is the point rather than a nicety. The screen used to draw coins + pendingCoins, so the biggest number in a game whose whole argument is there is no number that is not on the chain was the one number that was not. It also disagreed with the shop board two metres away, which grades affordability off the confirmed balance: a player could read 40, click a 25-coin upgrade, and be told they have 20 — which reads as the chain being broken when it was the client guessing.

Spending is graded against confirmed, available funds. The pending figure drains as real confirmations arrive rather than when banking starts, so it does not blink to zero mid-batch, and it drains by what the bundle actually paid — the server caps a bank that arrived too fast to be a human hand, so the presses asked for and the coins paid are not always the same figure.

Where things are

shared/catalogue.ts   what a press is worth and what upgrades cost — used by both halves
server/game.ts        the issuer: token, items, the batcher, the shop. The whole backend.
server/main.ts        one Bun server: the mock node at /rpc, the game at /game/*, the client at /
src/economy.ts        every line of Kei in the client
src/world.ts          Babylon: the button, the screen, the shopkeeper
src/screen.ts         what the two in-world screens draw

Read src/economy.ts to learn the SDK. Read server/game.ts to learn what a game server still has to do once it is not allowed to hold money.

shared/catalogue.ts is shared by both halves deliberately. The server is authoritative about payouts because it is the only side that can mint, but the client has to predict the same numbers to draw them, and two copies of that arithmetic would drift within an afternoon. Nothing in it is a balance — it is a price list.

No database, and what that buys

There is no persistent storage of any kind on the game server. Stop it, start it, and a player's coins and upgrades are still theirs, because they were never the server's to hold.

On load the client calls kei.wallet.summary() and rebuilds the player's upgrades from their on-chain item balances. There is no save file to corrupt, migrate, or lose.

What the server does own is what a game server should own: what a press is worth, and what things cost.

Configuration

VariableDefaultEffect
PORT7777Listen port
KEI_GAME_SEEDgenerated per runThe issuer seed. Fixing it keeps asset ids stable across restarts.
BUTTON_EXCHANGEonoff removes the exchange desk from the shop

Playing with payments off

The game has to be enjoyable with payments disabled, so that is a switch rather than a claim:

sh
BUTTON_EXCHANGE=off bun run dev

The exchange desk disappears and Kei buys nothing. Everything else — pressing, banking, claiming, buying upgrades with coins — is untouched, because coins come from playing and never from paying.

With no server running at all, the page still loads and the button still presses; it says on the screen that nothing is being banked.

Tests

sh
bun test

test/economy.test.ts runs the loop against a chain in-process. test/m4-native.test.ts runs the commit and claim path against a native node, which is what makes "the batching works" a checked statement rather than a description.

Known limits

  • The chain underneath is a mock. It dies when you stop the server, and nothing on it is worth anything. test/m4-native.test.ts is what connects this to a real node; the game itself does not.
  • Everything in-memory on the server — sessions, the observation ceiling, batches, faucet grants, orders, receipts — is gone on a restart, along with the mock chain itself. See Player rewards for the full list and each one's lifetime.
  • The issuer seed is generated per run unless KEI_GAME_SEED is set.
  • The pending figure is the client's own arithmetic until a bundle confirms it. It is shown as what is owed, never as what is held, and nothing in the game will let a player spend it.

Continue

  • Button fundamentals — the same loop as a runnable script: two seeds, one commit, one claim, no Babylon.
  • Button NPC shop — the purchase protocol: order, transfer, on-chain verification, mint, burn.
  • Player rewards — presses and mob drops as recipient-bound claims: batching, retries, serialized claiming, and the gaps.
  • Carpet Markets — what happens when players can issue things and trade them with each other.
  • Batch rewards reference — the commit and claim API this example is built around.
  • Integration model — the two halves, stated once, without a game around them.

The package is the source of truth for the API.