Skip to content

Install to a confirmed payment

Kei has two entry points: one for a player's browser and one for the game's server. A key signs only for its own account, so those roles never collapse into one.

Live on the public testnet

The installable SDK is kei-transaction@0.8.0, and it defaults to the public testnet at https://testnet.keicoin.org/rpc. Since 3 August 2026 that node accepts rooted-claim and swap blocks: a rooted claim lands, a second claim from the same account is refused, an offer locks its units, and one accept moves both legs — measured over the public URL, not inferred from CI.

Every sentence above is one command: bun run docs/playgrounds/testnet-live.ts publishes those blocks to that URL and asserts each of them. It is the public testnet proof, and it is the only playground on this site that uses the network.

It is still one node

The testnet is one rate-limited, best-effort dev node with weak consensus, published dev keys, no uptime promise, and no monetary value. There is no mainnet. Item stats are published in the current SDK. The create-kei-game@0.2.0 package on npm is a retired scaffolder and a different product from Create Kei MMO, which is an unpublished draft. Build against the API; do not ship a production economy on it.

Install

sh
bun add kei-transaction
sh
npm install kei-transaction
sh
pnpm add kei-transaction
sh
yarn add kei-transaction

The package is ESM, includes TypeScript types, and runs in browsers, Node.js, and Bun.

Choose the correct entry point

ContextStart withHolds
Player's browserKei.start()The player's seed
Game serverKei.server()The issuer's seed
ts
import { Kei } from 'kei-transaction'

// Browser: creates and persists a player wallet.
const kei = await Kei.start()
ts
import { Kei } from 'kei-transaction'

// Server only: refuses to run in a browser.
const game = await Kei.server({ seed: process.env.KEI_SEED })

Keep the issuer seed on the server

An issuer seed in client code gives anyone who can view the bundle unlimited minting authority. Kei.server() rejects browser environments on purpose.

Send a payment

The player signs the payment from their own wallet:

ts
const receipt = await kei.pay({
  to: gameAddress,
  amount: 0.05,
})

Persist receipt.hash with the order over your normal server channel. This is the player's send-block hash.

The game observes the confirmed payment on its server and delivers from its own account:

ts
game.onPayment(async ({ from, amount, hash: receiveHash }) => {
  const receive = await game.client.node.blockInfo(receiveHash)
  if (!receive || receive.type !== 'state' || !['open', 'receive'].includes(receive.subtype)) return

  await purchases.recordPayment({ sendHash: receive.link, receiveHash, from, amount })
  await reconcile(receive.link)
})

onPayment.hash is the game's receive-block hash, not receipt.hash; the receive block's link is the player's send hash. Persist the order and confirmed payment independently by that send hash, then call the same atomic, idempotent reconciliation path after either write. This handles a payment that confirms before the browser attaches it to the order and prevents duplicate delivery.

A Kei payment has no memo field in the current wire contract. The SDK rejects pay({ memo }) rather than silently dropping it. There is also no charge(someoneElse, amount): the player signs payment; the issuer signs delivery.

Economy helpers in the current umbrella

kei-transaction@0.8.0 depends on @keicoin/economy@0.2.2 and @keicoin/player-economy@0.1.2. A plain install reaches weighted loot-table drops through kei.economy and the player-owned stall through kei.shop. The earlier 0.6.0 umbrella first closed that installation gap; 0.5.0 did not reach the shop.

Weighted loot-table drops

The server and browser import the same declared table:

ts
import { defineDropTable } from 'kei-transaction'

export const dragonHoard = defineDropTable({
  id: 'dragon-hoard',
  drops: [
    { asset: { symbol: 'GOLD' }, amount: 50, weight: 60 },
    { asset: { symbol: 'SWORD' }, weight: 10 },
  ],
  nothing: 30,
  issuer: GAME_ADDRESS,
})

The game publishes one drop batch for the party; each player verifies and then claims their own award:

ts
const drop = await game.economy.drop(dragonHoard, party)
send(playerA, drop.awardFor(playerA))

const { symbol, quantity, chance } = await kei.economy.verifyDrop(award)
await kei.claims.add(award)

Not verifiable randomness

The roll happens on the game's server, and nothing here proves the declared weights were honoured. Verification proves that the published batch was bound to the table the player saw and that the award is an entry owed to that player; it does not prove the server rolled fairly.

The loot tables reference runs the whole batch — roll, verify, claim, the refusal of a rewritten table, and the close — as one file you can execute.

Player-owned shop

The player's own key lists, buys, cancels, or gifts. The game never takes custody of the item or the payment:

ts
await kei.shop.list({ item: 'sword', qty: 2, each: 120 })
const shelves = await kei.shop.browse()
await kei.shop.buy(shelves.listings[0])
await kei.shop.gift({ to: friend, item: 'sword' })

Current shop evidence

This surface has been exercised against Kei.mock() and over HTTP between two clients sharing only a URL. It has not yet been run against the public testnet. Do not infer public-testnet shop settlement from the rooted-claim and swap conformance results above.

The player shop reference runs a whole stall — list, browse, buy, the refusals, cancel, and gift — and asserts that the game server ends holding none of it.

Continue

The package is the source of truth for the API.