Skip to content

Loot and drops

World of Wonder no longer pays a kill into PlayerSchema.gold or turns a pickup into a character_inventory row. It also does not pay those rewards on-chain in a running server yet. Merged phase one removes the database as a second economy and fails closed while wallet ownership proof is unavailable.

This page separates three states that are easy to collapse into one claim:

  1. the old database reward path is gone from gameplay authority;
  2. a direct, idempotent chain-mint path exists and is tested behind a stub verifier;
  3. the running server wires proofUnavailable, so no character can bind a wallet and those rewards refuse today.

Before you begin

A running forkgit clone https://github.com/keicoin-org/world-of-wonder, then the run steps
Node20.17 or later
Which chainKEI_NETWORK=mock is the no-secret local mode
Current authoritysrc/server/kei/Inventory.ts
Legacy evidence onlysrc/server/kei/Legacy.ts reads old inventory, equipment, and gold rows so the player can be told they are inert
Behavioural proofnpm run test:inventory

What happens on a kill today

World of Wonder gameplay view with character, village, combat hotbar, chat, and HUD

This is the combat world where server-authored rewards originate. It is not proof that a reward reached a wallet.

Capture provenance — illustration, not runtime evidence

The revision this was captured from was never recorded, and the image is not in the World of Wonder repository, so the state on screen cannot be re-created. Under the screenshot evidence contract that makes it an illustration of the running game and not proof of anything — in particular it is not proof that any reward reached a wallet.

FieldRecorded
Filedocs/public/img/docs/world-of-wonder-gameplay.webp, 1600 × 900 WebP
Integritysha256sum docs/public/img/docs/world-of-wonder-gameplay.webp047fcafb702fbc43f26e302c747303624a622a280f89e31dc7dea0682ef5ab35
Repositorykeicoin-org/world-of-wonder
Revision at captureNot recorded. The capture cannot be reproduced.
Command or URLNot recorded. The run steps start a server and a client, and https://mmo.keicoin.org serves a deployed one; which of the two this came from is unknown.
Network or mock modeNot recorded. The fork defaults to the public testnet and KEI_NETWORK=mock is the local mode.
ViewportNot recorded. The stored image is 1600 × 900; the browser and device pixel ratio it was taken at are unknown.
Scenario stateNot recorded. Character, position, level and HUD contents are per-account database state.
Alt text"World of Wonder gameplay view with character, village, combat hotbar, chat, and HUD"
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.

The upstream loot table still decides what can drop. Enemy records carry a gold range and point at item rows such as:

ts
new LootTableEntry('sword_01', 10, 1, 1, 1, 1)
//                  item key   chance  quantity  level

The server still owns that game-design decision and still resolves kills. What changed is where the result is allowed to go:

ResultCurrent default-branch behaviour
ExperienceAdded to character progression in the database. Experience is not money or an item.
Kill golddropCTRL.addGold() asks the inventory authority to pay reward id kill:<enemy session id>. The request is refused because the character has no proven wallet. Nothing is added to PlayerSchema.gold.
Item rollA server-authored loot entity can still appear on the ground.
PickuppickupItem() checks for a bound wallet before removing the entity. With the shipped verifier there is no binding, so the item stays on the ground and nothing is minted.
Quest gold or itemsThe quest is server-resolved, but its quest:<character>:<quest> payment is refused for the same missing binding. Nothing is written into legacy economy rows.

The refusal is visible in chat. A silent fallback would make a database number look like ownership again, so there is deliberately no fallback.

The old rows are evidence, not authority

character_inventory, character_equipment, and the old gold column are not deleted during phase one. They may contain a player's pre-migration state, and deleting or rewriting them before a wallet migration exists would destroy the only record of what needs to be resolved.

They are read once on join to report how many legacy rows exist. They are not loaded into the room's usable bag, do not authorize an equip or consume, and are not rewritten by autosave. Inserting a sword directly with SQL changes no chain holding and grants no gameplay authority.

This is the boundary to keep:

StateUsable ownership?
A legacy inventory or equipment rowNo — retained migration evidence only
A client-supplied Kei addressNo — an address is not proof of control
A chain holding under a proven addressYes in Inventory.authorize(); the running game has no proof route yet
An item locked in an open offerNo for gameplay — the chain no longer reports it as spendable ownership

What is already implemented behind the refusal

Inventory.ts contains the service phase two can wire rather than a database workaround:

  • challenge(characterId) creates a server-issued, domain-separated, single-use challenge.
  • bind(characterId, proof) accepts only a verifier-approved address/signature pair for that exact challenge.
  • authorize(characterId, item, quantity) re-reads the bound address's holdings; it does not cache a balance that can become stale when another tab lists the item.
  • pay(characterId, reward) records a server-authored reward id before minting gold or items to the bound address.

The payment id is what prevents a replay from minting twice. Kills use the enemy session id, pickups use the loot entity id, and quests use the character and quest key. An in-process settling set closes the two-messages-in-one-tick race; reward_payments closes the ordinary restart replay.

Recording before minting chooses the safe failure direction: a crash in between can underpay one reward, but cannot reopen it and mint without bound after restart. That is a support problem instead of a printing press.

The verifier and gameplay wiring are still missing

The service has a proof interface, but src/server/index.ts supplies proofUnavailable. There is no browser route that asks for the challenge, no SDK ownership-challenge signing helper, and no server verifier connected to gameplay. authorize() is proved directly in the test; equipping and consuming are not a live end-to-end wallet flow.

Dropping also remains refused even after a future binding. A wallet-held item cannot become a ground mint merely because the room received a message; the player must sign the item away or the result would be a duplicate.

Direct mint now, rooted claims later

The phase-one service uses issuer-signed grant and deliver calls. That is simple and correct for the construction-scale test, but every reward serialises through the issuer's account chain. A busy game should batch rewards with rooted claims rather than turn every kill into an issuer write.

That later path has a different custody split:

ConcernWhose job
Decide whether the kill happenedColyseus, server-side
Decide the rewardThe server's loot table — design, not custody
Publish a batch rootThe issuer
Deliver the recipient-bound proofThe authenticated game session
Publish the claimThe player's wallet
Refuse forged, altered, reused, or misdirected claimsThe ledger

Rooted claims remove the issuer write bottleneck. They do not prove a kill happened, bind a socket to a wallet, or make a proof bundle safe to publish. Those remain game-authentication responsibilities.

Check the boundary

sh
git clone https://github.com/keicoin-org/world-of-wonder
cd world-of-wonder
npm ci
npm run test:inventory

The named checks prove that the shipped verifier refuses a forged proof, a challenge must be issued and is single-use, a database sword and gold authorize nothing, a minted sword does authorize after a stub-verified binding, an open offer locks it out, and the same reward id pays once across a replay, a same-tick duplicate, and a fresh authority reading the existing payment records.

They do not prove the missing browser-to-server wallet binding, a live pickup, a live equip, crash recovery between the payment record and the mint, scale, or a public deployment.

Next steps

The package is the source of truth for the API.