# AGENTS.md — integrating Kei

For an agent adding a game economy to a project. Read [llms.txt](https://keicoin.org/llms.txt)
first if you have not decided whether Kei is the right tool; it says plainly when
it is not.

**Status: M1 of eleven.** The SDK is real and runs end to end. The chain underneath it is an in-memory mock — there is no public network yet, and nothing anywhere holds value. Do not ship an economy that holds
real value on this.

## Decide first

Answer these before writing code. Two of them cannot be changed later.

| Question | Consequence |
|---|---|
| Is there a server process that can hold a secret? | Required. The issuer seed cannot be in the browser, so a purely static game cannot issue. |
| `transfer: 'open'`, `'issuer-only'`, or `'none'`? | **Immutable at issuance.** `open` means a player market can and eventually will exist. `none` is soulbound. |
| Currency symbol? | Asset ids are derived as `H(issuer_pubkey ‖ symbol)`, so this fixes the id forever. |
| What is the sink? | A currency that is only minted inflates. The chain will not fix that. |

## Procedure

1. **Install.** `bun add kei-transaction`. Nothing to sign up for.
2. **Generate an issuer seed** with `randomSeed()`. Put it in the server's
   environment. Never in the client bundle, never in a repo, never in a log.
3. **Create the issuer** — `Kei.server({ seed: process.env.KEI_SEED })`. It
   throws if it detects a browser. Do not work around that; it is the point.
4. **Fund it.** Issuing burns 1,000 Kei per asset. On testnet, `faucet()`.
5. **Issue the token** with `game.token.issue(...)`. Idempotent per
   (issuer, symbol) — calling it again returns the existing token rather than
   burning another 1,000 Kei, so it is safe on every boot.
6. **Create the player** — `Kei.start()` in the browser. It self-provisions:
   generates a seed, persists it to browser storage, and is ready to transact.
7. **Wire the purchase as two halves.** Player: `kei.pay({ to, amount, memo })`.
   Issuer: `game.onPayment(...)` then `mint`. Never try to move a player's
   funds from the server.
8. **Rewards in bulk go through `commit`**, not a loop of `mint`. Deliver
   `drop.proofFor(address)` to each player however you already talk to them;
   they call `kei.claims.add(bundle)`.
9. **Read balances from the chain**, with `balanceOf` / `ownedBy`. Do not
   cache them in your own table and treat that as truth.

## Failure modes, and what they actually mean

| Symptom | Cause | Fix |
|---|---|---|
| `Kei.server()` throws about a browser | The issuer seed reached the client bundle | Move it to a server. This is a total compromise if shipped. |
| `Not enough Kei` on `issue` | The issuer holds less than the 1,000 Kei burn | Fund the issuer address; on testnet call `faucet()`. |
| A mint appears not to arrive | Assets arrive **receivable**; the recipient's own signed block collects them | Nothing — the SDK collects in the background. In a test, `await kei.sync()`. |
| `Root ... has already been published` | Two commits produced the same root | Update the SDK; commits are salted so identical batches are distinct drops. |
| A transfer is rejected | The token was issued `issuer-only` or `none` | Nothing. That is protocol-enforced and immutable. It was decided at issuance. |
| Balances disagree with your database | You kept a database of balances | Delete it. The chain is the ledger. |

## Do not

- Do not put the issuer seed anywhere a browser can reach.
- Do not implement `charge(player, amount)`. It cannot exist.
- Do not hold player balances on your game server.
- Do not mint in a loop for a batch reward.
- Do not tell a user this is production-ready. It is M1 of eleven, on a mock
  chain, with no mainnet.

## Verify your work

The demo at https://keicoin.org/examples/button exercises every primitive — issue,
mint, transfer, items, balanceOf, commit and claim — and its source is written to
be read. If your integration looks structurally different from it, check why
before shipping.
