@idosgames/mcp 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli.js +2 -2
  2. package/package.json +1 -1
  3. package/registry/host.json +7 -7
  4. package/registry/index.json +437 -110
  5. package/registry/modules/ai.json +65 -0
  6. package/registry/modules/board-game.json +3 -3
  7. package/registry/modules/boosts.json +66 -0
  8. package/registry/modules/character.json +4 -5
  9. package/registry/modules/chat.json +4 -5
  10. package/registry/modules/collections.json +69 -0
  11. package/registry/modules/coop-events.json +67 -0
  12. package/registry/modules/craft.json +4 -5
  13. package/registry/modules/deals.json +4 -5
  14. package/registry/modules/events.json +8 -13
  15. package/registry/modules/friends.json +5 -5
  16. package/registry/modules/idle-rpg.json +3 -3
  17. package/registry/modules/inventory.json +15 -18
  18. package/registry/modules/invite.json +7 -7
  19. package/registry/modules/leaderboard.json +4 -5
  20. package/registry/modules/lootboxes.json +67 -0
  21. package/registry/modules/mailbox.json +71 -0
  22. package/registry/modules/marketplace.json +4 -5
  23. package/registry/modules/mini-game.json +2 -2
  24. package/registry/modules/multiplayer.json +74 -0
  25. package/registry/modules/premium.json +67 -0
  26. package/registry/modules/profile.json +9 -10
  27. package/registry/modules/pvp.json +4 -5
  28. package/registry/modules/quests.json +69 -0
  29. package/registry/modules/rewards.json +15 -21
  30. package/registry/modules/season.json +8 -13
  31. package/registry/modules/store.json +4 -5
  32. package/registry/modules/tutorials.json +4 -5
  33. package/registry/modules/voxelcraft.json +15 -11
  34. package/registry/modules/workshop.json +4 -5
  35. package/registry/skills/blockchain-system.json +1 -1
  36. package/registry/skills/idosgames-compose-modules.json +2 -2
  37. package/registry/skills/idosgames-getting-started.json +1 -1
  38. package/registry/skills/idosgames-module-contract.json +2 -2
  39. package/registry/skills/idosgames-ui-kit.json +2 -2
  40. package/registry/skills/mailbox-system.json +6 -0
  41. package/registry/skills/multiplayer-realtime.json +2 -2
  42. package/registry/skills/workshop-system.json +1 -1
@@ -7,7 +7,6 @@
7
7
  },
8
8
  "catalog": {
9
9
  "type": "feature",
10
- "inBase": true,
11
10
  "summary": "Workshop: players and the publisher share content (maps, levels, skins, models) — free, for in-game resources, or unlocked by holding items.",
12
11
  "description": "A ready catalog of user-generated content for ANY game. Players publish what they made (a world, a level, a skin, any file the title allows), set how others get it — free, a price in in-game resources (several options at once, the author receives it minus commission), or 'hold N of this item / currency to unlock' (nothing is charged; while held or once) — and others browse, filter, acquire, open, like, favorite, follow authors and report. The publisher configures content types, formats, limits, commission and moderation in the Workshop section of the title config, curates featured collections and publishes official content from the dashboard. A game plugs its own content in by registering a handler with ctx.content.registerType({ type, label, listLocal, capture, open, modeId }) — the Workshop never imports the game. VoxelCraft registers 'voxelcraft.world'.",
13
12
  "provides": [
@@ -40,12 +39,12 @@
40
39
  "version": "0.1.0"
41
40
  },
42
41
  "dependencies": {
43
- "@idosgames/core": "0.19.0",
44
- "@idosgames/module-sdk": "0.6.0",
42
+ "@idosgames/core": "0.19.1",
43
+ "@idosgames/module-sdk": "0.7.0",
45
44
  "@idosgames/react": "0.3.0",
46
45
  "react": "19.2.7"
47
46
  },
48
- "contentHash": "8708fd1f761188580603bd1e2cca47209c2d618a9c82a5530cca9c083bca19e6",
47
+ "contentHash": "f6c4cf4ececb9f9c0167b5c2598bdc4d471f364781ffc62cff7481e8a25751e4",
49
48
  "files": [
50
49
  {
51
50
  "path": "i18n.ts",
@@ -65,7 +64,7 @@
65
64
  },
66
65
  {
67
66
  "path": "module.meta.json",
68
- "content": "{\n \"id\": \"workshop\",\n \"type\": \"feature\",\n \"inBase\": true,\n \"summary\": \"Workshop: players and the publisher share content (maps, levels, skins, models) — free, for in-game resources, or unlocked by holding items.\",\n \"description\": \"A ready catalog of user-generated content for ANY game. Players publish what they made (a world, a level, a skin, any file the title allows), set how others get it — free, a price in in-game resources (several options at once, the author receives it minus commission), or 'hold N of this item / currency to unlock' (nothing is charged; while held or once) — and others browse, filter, acquire, open, like, favorite, follow authors and report. The publisher configures content types, formats, limits, commission and moderation in the Workshop section of the title config, curates featured collections and publishes official content from the dashboard. A game plugs its own content in by registering a handler with ctx.content.registerType({ type, label, listLocal, capture, open, modeId }) — the Workshop never imports the game. VoxelCraft registers 'voxelcraft.world'.\",\n \"provides\": [\n \"player-made content catalog (maps, levels, skins, models, any file type the title allows)\",\n \"publish a map or level made in the game\",\n \"sell content for in-game resources with author royalties minus commission\",\n \"free content sharing\",\n \"unlock content by holding an item or currency without spending it\",\n \"likes, favorites and following content creators\",\n \"report content and auto-hide after reports\",\n \"publisher featured collections and official content\",\n \"download and open shared content in the game\"\n ],\n \"tags\": [\n \"workshop\",\n \"ugc\",\n \"user-generated-content\",\n \"maps\",\n \"levels\",\n \"mods\",\n \"sharing\",\n \"marketplace\",\n \"creators\",\n \"social\"\n ],\n \"author\": { \"name\": \"iDos Games\", \"url\": \"https://idosgames.com\" },\n \"version\": \"0.1.0\"\n}\n"
67
+ "content": "{\n \"id\": \"workshop\",\n \"type\": \"feature\",\n \"summary\": \"Workshop: players and the publisher share content (maps, levels, skins, models) — free, for in-game resources, or unlocked by holding items.\",\n \"description\": \"A ready catalog of user-generated content for ANY game. Players publish what they made (a world, a level, a skin, any file the title allows), set how others get it — free, a price in in-game resources (several options at once, the author receives it minus commission), or 'hold N of this item / currency to unlock' (nothing is charged; while held or once) — and others browse, filter, acquire, open, like, favorite, follow authors and report. The publisher configures content types, formats, limits, commission and moderation in the Workshop section of the title config, curates featured collections and publishes official content from the dashboard. A game plugs its own content in by registering a handler with ctx.content.registerType({ type, label, listLocal, capture, open, modeId }) — the Workshop never imports the game. VoxelCraft registers 'voxelcraft.world'.\",\n \"provides\": [\n \"player-made content catalog (maps, levels, skins, models, any file type the title allows)\",\n \"publish a map or level made in the game\",\n \"sell content for in-game resources with author royalties minus commission\",\n \"free content sharing\",\n \"unlock content by holding an item or currency without spending it\",\n \"likes, favorites and following content creators\",\n \"report content and auto-hide after reports\",\n \"publisher featured collections and official content\",\n \"download and open shared content in the game\"\n ],\n \"tags\": [\n \"workshop\",\n \"ugc\",\n \"user-generated-content\",\n \"maps\",\n \"levels\",\n \"mods\",\n \"sharing\",\n \"marketplace\",\n \"creators\",\n \"social\"\n ],\n \"author\": { \"name\": \"iDos Games\", \"url\": \"https://idosgames.com\" },\n \"version\": \"0.1.0\"\n}\n"
69
68
  }
70
69
  ]
71
70
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "blockchain-system",
3
3
  "description": "Bridge in-game assets on-chain in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.blockchain (BlockchainService): load blockchain network/config definitions, load the player's on-chain state (linked wallets, pending withdrawals, KYC, stats), deposit a token or NFT from a wallet into the game, request a token or NFT withdrawal out to a wallet, read on-chain transaction history, retry a still-pending withdrawal's signature, confirm a withdrawal's on-chain tx hash, and donate crypto to the developer or a users' pool. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants crypto wallets, NFT deposits/withdrawals, token bridging, on-chain asset transfers, KYC status, or otherwise touches client.blockchain, BlockchainService, BlockchainDefinitions, UserBlockchainState, DepositTokenResponse, TokenWithdrawalResponse, or NFTWithdrawalResponse — even if they don't name the module explicitly.",
4
- "content": "---\nname: blockchain-system\ndescription: >-\n Bridge in-game assets on-chain in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.blockchain (BlockchainService): load blockchain\n network/config definitions, load the player's on-chain state (linked\n wallets, pending withdrawals, KYC, stats), deposit a token or NFT from a\n wallet into the game, request a token or NFT withdrawal out to a wallet,\n read on-chain transaction history, retry a still-pending withdrawal's\n signature, confirm a withdrawal's on-chain tx hash, and donate crypto to\n the developer or a users' pool. Use this whenever the user is working in\n the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants crypto wallets, NFT\n deposits/withdrawals, token bridging, on-chain asset transfers, KYC status,\n or otherwise touches client.blockchain, BlockchainService,\n BlockchainDefinitions, UserBlockchainState, DepositTokenResponse,\n TokenWithdrawalResponse, or NFTWithdrawalResponse — even if they don't name\n the module explicitly.\n---\n\n# Blockchain system (iDosGames TS SDK)\n\nThe Blockchain module bridges in-game assets to and from real wallets on\nsupported chains (EVM and Solana networks). A player can deposit a token or\nNFT they already sent on-chain (crediting their in-game balance/inventory),\nor request a withdrawal that pays an in-game token/NFT out to their wallet\n(debiting their in-game balance/inventory and producing a signature the\nplayer submits on-chain themselves). Everything is **server-authoritative**:\nthe client reports/requests, the backend validates the transaction against\nthe chain, applies rules (network enabled, KYC, account-safety policy), and\nthe SDK mirrors the confirmed result into a local cache your UI reads. You\nnever credit/debit balances yourself.\n\nThis skill is for **using** the production `BlockchainService`, not for\nporting or extending it, and not for signing/broadcasting transactions\nyourself — this SDK reports deposits and requests withdrawals; actually\nsending the on-chain transaction (the deposit transfer, or broadcasting a\nwithdrawal signature) happens with a wallet SDK outside this client.\n\n> **The on-chain half is the `@idosgames/wallet` companion package.** It\n> connects browser & mobile wallets (EVM via wagmi/viem/WalletConnect, Solana\n> via wallet-adapter) and runs the exact RewardPool contract calls, threading\n> them through this service's request → submit → confirm / approve → deposit →\n> report lifecycle. If the user wants to actually connect a wallet and move\n> tokens/NFTs (not just call `client.blockchain.*`), reach for that package —\n> see its README. Everything below documents the server-authoritative\n> `client.blockchain` surface that `@idosgames/wallet` builds on. The wallet\n> package's EVM surface covers both NFT standards: `submitEvmNftWithdrawal` /\n> `depositNftEvm` (+ `erc1155Abi`) for ERC-1155 collections, and\n> `submitEvmNftWithdrawal721` / `depositNftEvm721` (+ `erc721Abi`) for\n> ERC-721 unique-item collections (4-arg `safeTransferFrom`, no `id`/`amount`\n> — the token is always qty 1). `submitEvmTokenWithdrawal`'s `withdrawERC20`\n> call now also threads `sig.BurnAmount` through (see\n> [Burn on withdrawal](#gotchas) below) — the ABI/contract call order changed,\n> so an app pinned to an older `@idosgames/wallet` build will revert on-chain\n> against an updated RewardPool contract.\n\n> **Never import `@idosgames/wallet/react` (or `/react/solana`) from a file\n> that loads on startup.** Those subpaths pull in Reown AppKit, and AppKit is\n> deliberately _not_ a dependency of a generated project — the live preview\n> resolves every declared dependency up front and times out on AppKit's tree.\n> A static import therefore blanks the preview before any game code runs\n> (`Could not find dependency: '@reown/appkit-adapter-wagmi'`), while the real\n> build stays green — that mismatch is the signature of this mistake.\n> Two rules keep both working:\n>\n> - **Sign-in button:** import `LazyWalletLogin` / `LazySolanaWalletLogin` from\n> `@idosgames/wallet/react/lazy` (that entry has no AppKit in its graph; it\n> also re-exports the chains as plain objects — never import chains from\n> `wagmi/chains` or `viem/chains`, that barrel breaks the preview too).\n> - **In-game deposit/withdraw panel:** import `LazyWalletPanel` from\n> `@idosgames/wallet/react/lazy` and pass it the authenticated `client` (a\n> prop, like the login button — never a module context). Same lazy contract:\n> AppKit stays out of the startup graph. Don't hand-roll your own\n> `await import(\"./PanelImpl\")` wrapper — `LazyWalletPanel` is that wrapper.\n> Because the wallet config is memoised per WalletConnect project id, the\n> panel reuses the wallet the player connected at sign-in: same store, so it\n> opens already-connected, no second tap and no second modal. This is exactly\n> how the board-game and idle-rpg modules wire their `WalletPanel.tsx`.\n\n> **The game cannot reach the wallet while it is embedded in an idosgames.com iframe** (owner's\n> decision, 2026-09-17 — deposit/withdraw goes through the buttons in the site's own wallet card).\n> You don't need to special-case this: `LazyWalletPanel` renders nothing there, and\n> `LazyWalletLogin` / `LazySolanaWalletLogin` stay a working \"Sign in with wallet\" button — inside\n> the frame the SITE's connected wallet signs the engine's login message for the game\n> (`loginWithWalletViaPlatform` from `@idosgames/wallet`, used by the buttons automatically). Never\n> show a \"use idosgames.com\" notice inside the game. Every flow function that talks to the wallet\n> directly (`depositTokenEvm`, `withdrawTokenEvm`, `loginWithWalletEvm`, their Solana equivalents,\n> `payWithWalletEvm`) refuses with `WALLET_USE_SITE_PANEL` inside a frame. A game opened by its own\n> direct link (not embedded) is unaffected — every one of these works exactly as before there.\n\n### Login mode and play access by token balance (`cfg.Blockchain.PlayAccess`)\n\nA title can require players to HOLD tokens to play. The publisher picks one of three login modes:\n\n| `Mode` | Who can sign in | Balance check |\n| --------------------- | ----------------------------- | -------------------------------------------- |\n| `Open` (default) | every method, guests included | none |\n| `AnyWithLinkedWallet` | every method except guest | the wallet the player linked by signature |\n| `WalletOnly` | wallet login only | the wallet they signed in with — and only it |\n\nIn `WalletOnly` the account must BE the wallet's account: an account created another way before the\nmode was switched on gets `PLAY_ACCESS_WALLET_LOGIN_ONLY` on every check, and `linkWallet` refuses\nany wallet but the account's own with the same code — don't offer \"link wallet\" there, offer\n\"log out and sign in with your wallet\".\n\nRequirements are `TokenGates` (`CurrencyID`, `MinBalance`, `BalanceSource`: `OnChain` wallet /\n`InGame` game balance — no wallet needed / `Combined`; `NetworkID` for wallet ones). The SERVER\nchecks the balance on every login and session refresh — silently, no signature — and on\n`client.blockchain.confirmPlayAccess()`. Gameplay calls without a valid pass fail with HTTP 403\n`PLAY_ACCESS_REQUIRED`; `Blockchain` itself, account linking and the login-time `User` reads stay open.\n\n**The host template already handles all of it — don't rebuild it.** `mountHost` shows the\nplay-access screen (`renderPlayAccess`, the template's `src/PlayAccessScreen.tsx` over\n`DefaultPlayAccessScreen`) instead of the game whenever `client.auth.playAccess?.Granted === false`\nor `playAccess:required` fires, and the template's login screen hides the methods the mode refuses\n(`client.auth.getAuthOptions()` → `PlayAccessMode`). \"Link wallet\" on that screen is\n`LazyWalletLogin mode=\"link\"` (`renderWalletLink` in `src/walletLogin.tsx`); inside the\nidosgames.com frame the site's wallet signs the link (`linkWalletViaPlatform`).\n\nClient surface, if you roll your own screen: `client.auth.playAccess` (+ `playAccess:changed`),\n`client.blockchain.getPlayAccess()` (mode, requirements, the player's signature-linked wallets, last\ncheck), `client.blockchain.confirmPlayAccess(networkID?)` (choose a linked wallet and check now),\n`client.auth.linkWallet(address, networkID, signature?)` (two steps; the second returns the pass\nchecked against that wallet), `linkWalletEvm` / `linkWalletSolana` / `linkWalletViaPlatform` in\n`@idosgames/wallet`. Refusal codes: `PLAY_ACCESS_WALLET_REQUIRED`,\n`PLAY_ACCESS_WALLET_NETWORK_NOT_GATED`, `PLAY_ACCESS_GUEST_DISABLED`, `PLAY_ACCESS_WALLET_LOGIN_ONLY`,\n`PLAY_ACCESS_CHECK_FAILED` (the RPC could not be read — NOT an empty wallet; check again), or the\nrequirement's own `DenyMessage`. A login or refresh never fails for lack of balance — in every mode\nthe session is issued and the game stays closed until the pass is granted.\n\n### Operation category (`game_topup` by default)\n\nThe updated RewardPool contract tags every deposit/withdrawal with a string\n**category** (default `\"game_topup\"`; `\"community_reward\"` is the other known\nvalue, and arbitrary strings are allowed). On a **withdrawal** the server signs\nthe category into the on-chain hash and returns it on the signature payload\n(`EvmSignature.Category` / `.TitleID`); whoever submits the transaction **must\npass the same value on-chain verbatim** or the contract rejects the signature —\n`@idosgames/wallet` does this for you. Pass it as the optional last arg to\n`requestTokenWithdrawal`/`requestNFTWithdrawal` (omit → `\"game_topup\"`). On a\n**deposit** the category is read back from the on-chain transaction, so you\ndon't pass it to `depositToken`/`depositNFT`. It also appears on transaction\ndocuments as `Category`. Import the constants from `@idosgames/core`:\n`BlockchainOperationCategory.GameTopUp` / `.CommunityReward`.\n\n## Mental model: deposits vs. withdrawals\n\n- **Deposit** = the player already sent tokens/an NFT to the platform's pool\n or vault address on-chain. The client then calls `depositToken`/`depositNFT`\n with that transaction's hash so the backend can verify it and credit the\n player in-game. One-shot: the credit happens directly on a successful call.\n- **Withdrawal** = the player wants an in-game token/NFT sent out to their\n wallet. The client calls `requestTokenWithdrawal`/`requestNFTWithdrawal`,\n which **debits in-game immediately** and returns a signed payload\n (`EvmSignature` or `SolanaSignature`) the player's wallet must submit\n on-chain to actually receive the asset. This is a **multi-step, async\n flow** — see [Recipes](#recipes) for the full lifecycle, including what to\n do when the on-chain submission fails.\n\nBoth flows are per-network: every call takes a `networkID` that must match one\nof the title's configured `Networks` (EVM or Solana), each with its own\ndeposit/withdrawal enable flags, contract/vault addresses, and (for NFTs) a\ncollection binding to an item catalog. Withdrawals additionally run through a\nlong chain of server-side gates (balances, per-network minimums, account\nsafety, KYC, daily/monthly compliance limits, a collective title-wide pool\ncap, and platform commission) — see\n[Withdrawal gates](#withdrawal-gates-what-can-reject-a-request) for the full\nlist with verbatim error strings.\n\nFor the full config/state field shapes (network definitions, NFT collection\nbindings, KYC tiers, transaction documents, signature payloads), read\n[references/data-model.md](references/data-model.md). You do **not** need it\nto call the methods — only to drive richer UI (network pickers, KYC gates,\ntransaction history tables).\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any auth.* method\n\nconst blockchain = client.blockchain; // the BlockchainService\n```\n\nEvery blockchain method requires an authenticated session. Without one they\nreturn `{ ok: false, reason: \"unauthorized\" }` — they do not throw.\n\n## Methods\n\nAll methods return `Promise<OperationResult<T>>`: a discriminated union that\nis either `{ ok: true, data }` or `{ ok: false, reason, error }`. Always\nbranch on `result.ok` before touching `result.data`. `reason` is one of\n`\"client\"` (missing/empty required arg — rejected before any network call),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\n600ms client-side throttle window), `\"connection\"` (transient, offer Retry),\n`\"validation\"` (response/schema drift), or `\"server\"` (backend rejected it —\n`error` carries the exact backend message). Withdrawals in particular can be\nrejected by a long chain of server-side gates — see\n[Withdrawal gates](#withdrawal-gates-what-can-reject-a-request) below for the\nfull list with verbatim error strings.\n\n| Method | Purpose | `data` on success |\n| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------- |\n| `getDefinitions()` | Load the title's blockchain config: networks, NFT bindings, crypto currencies. | `BlockchainConfigResponse` |\n| `getUserState()` | Load this player's on-chain state: linked wallets, pending withdrawals, KYC, stats, crypto balances. | `UserBlockchainStateResponse` |\n| `depositToken(networkID, transactionHash)` | Report an on-chain token transfer; credits the matching crypto balance. | `DepositTokenResponse` |\n| `depositNFT(networkID, transactionHash)` | Report an on-chain NFT transfer; grants the matching in-game item. | `DepositNFTResponse` |\n| `requestTokenWithdrawal(currencyID, networkID, walletAddress, amount, category?)` | Debit a crypto balance and get a signed payload to withdraw on-chain. | `TokenWithdrawalResponse` |\n| `requestNFTWithdrawal(itemID, networkID, walletAddress, amount, category?, level?, itemInstanceID?)` | Consume an in-game item and get a signed payload to withdraw the NFT on-chain. | `NFTWithdrawalResponse` |\n| `getTransactionHistory(limit?)` | Load recent token + NFT transaction documents (default limit 50). | `TransactionHistoryResponse` |\n| `retryWithdrawal(titleTransactionID)` | Re-issue a fresh signature for a still-`Pending` withdrawal without re-debiting. | `RetryWithdrawalResponse` |\n| `confirmWithdrawal(titleTransactionID, onChainTransactionHash)` | Tell the backend the signed withdrawal was submitted on-chain, with its tx hash. | `ConfirmWithdrawalResponse` |\n| `donateToDeveloper(networkID, transactionHash)` | Report an on-chain transfer as a donation to the developer pool (no personal credit). | `DonationResponse` |\n| `donateToUsersPool(networkID, transactionHash)` | Report an on-chain transfer as a donation to the users' pool (no personal credit). | `DonationResponse` |\n\nAll string args (`networkID`, `transactionHash`, `currencyID`,\n`walletAddress`, `amount`, `itemID`, `titleTransactionID`,\n`onChainTransactionHash`) are required and checked client-side before any\nnetwork call — an empty one short-circuits with `reason: \"client\"`. `amount`\nis a decimal string for token withdrawals and an integer-as-string for NFT\nwithdrawals / not used for deposits (deposit amounts come from the verified\non-chain transaction, not from the client). `getTransactionHistory(limit)`\ndefaults to `50`, is capped at **200** server-side (values above are silently\nclamped, values `<= 0` fall back to 50), and is sent as `Amount` on the wire\n(reused request field, not an actual currency amount).\n\n`requestNFTWithdrawal`'s trailing `level`/`itemInstanceID` are both optional\nand only matter for NFT catalogs with leveled or unique (ERC-721) bindings:\n`level` selects which leveled instance/tokenId to withdraw (omit or `1` →\nbase level, prior behavior); `itemInstanceID` is **required** when the\nitem's NFT binding is ERC-721 — it tells the server exactly which\nunstackable instance to debit and tokenize (preserving its Level/RemainingUses/\nCustomData in the on-chain registry via a separate ItemBridge contract).\nBoth are ignored for stackable/ERC-1155 items.\n\nOn success, most methods **mirror the confirmed change into the cache and\nemit an event** — see the next section for exactly which cache each method\ntouches, since it's not uniform across this module.\n\n## Withdrawal gates (what can reject a request)\n\n`requestTokenWithdrawal` / `requestNFTWithdrawal` run through a long chain of\nserver-side checks, each a `reason: \"server\"` failure with a specific `error`\nstring. Surface the string; don't try to pre-validate all of these\nclient-side — the gate list can change without a client update:\n\n- **Global kill switch** — withdrawals can be turned off platform-wide\n independently of any per-network/per-currency flag: `\"Withdrawals are\ncurrently disabled.\"`\n- **Network / currency / binding disabled** — `\"Withdrawals disabled for this\nnetwork.\"`, `\"Withdrawals are disabled for currency '{id}'.\"`, `\"This\ncurrency cannot be withdrawn in this network.\"`, or (currency under\n maintenance) `\"Currency '{id}' is under maintenance. Try again later.\"`\n- **Per-network minimum** — every currency has a `MinWithdraw` for each\n network it's bound to (`CryptoNetworkBinding.MinWithdraw`, see the\n currency-system skill's data-model for the full shape): `\"Minimum withdraw\nis {MinWithdraw} {currencyID}.\"`\n- **Insufficient balance** — `\"Not enough balance: have {available}\n{currencyID}, need {amount}.\"` (tokens) or `\"Not enough items: have {owned},\nneed {amount}.\"` (NFTs).\n- **Account safety** (`BlockchainAccountSafetyPolicy`, read-only in\n `getDefinitions()`'s `AccountSafety` block) — applies to withdrawals only,\n never deposits: account younger than `MinAccountAgeDays` →\n `\"Account is too fresh. Try again later.\"`; banned account → `\"Account is\nbanned. Contact support.\"`; withdrawing to a wallet address another account\n already used, when `MultiAccountCheckEnabled` +\n `BanOnSharedWithdrawalAddress` are both on, **bans the account on the\n spot** and returns `\"Account banned. Contact support.\"`\n- **Compliance / KYC** (`CryptoCurrencyDefinition.Limits`, per currency) —\n checked in USD-equivalent of the requested amount:\n - Above `Limits.KycRequiredAboveUsd` without `Kyc.Status === \"Verified\"` →\n `\"KYC verification required for withdrawals above {threshold} USD.\"`\n - This UTC calendar day's spend would exceed `Limits.DailyWithdrawUsd`\n (window resets at 00:00 UTC, not a rolling 24h window) →\n `\"Daily withdraw limit exceeded ({spentSoFar} + {thisAmount} >\n{dailyLimit} USD).\"`\n - This UTC calendar month's spend would exceed `Limits.MonthlyWithdrawUsd`\n (window resets 00:00 UTC on the 1st) → `\"Monthly withdraw limit exceeded\n({spentSoFar} + {thisAmount} > {monthlyLimit} USD).\"`\n - Any `Limits` field can be absent/null, which disables that specific\n check for that currency. The daily/monthly counters live server-side on\n `UserCryptoCurrencyState.Compliance` (not exposed as its own client\n method) and reset at UTC day/month boundaries — there is no way to read\n \"USD spent so far today\" from the client ahead of a request; read it off\n a rejection's `error` string instead.\n- **Collective pool cap** — independent of the player's own balance, the\n title's whole player-withdrawable pool for that (network, currency) pair\n can be exhausted: `\"Title users-withdrawable limit reached: available\n{available} {currencyID}, requested {amount}.\"` This is a title-wide\n economic limit, not specific to one player — if you see it, don't retry\n immediately.\n- **Platform commission** — a platform-wide withdrawal commission percent can\n reduce the net payout; if it would consume the entire requested amount,\n the request is rejected outright: `\"Withdrawal amount is fully consumed by\nplatform commission.\"` Otherwise the withdrawal proceeds and\n `NetAmountNative` reflects the amount after commission (see\n [Gotchas](#gotchas)).\n\nNone of these are configurable or visible as a single \"can I withdraw right\nnow\" flag — the practical pattern is: build the request, call it, and render\n`error` on failure. Use `getDefinitions()`'s `AccountSafety` block and the\ncurrency's `Limits` (from `getDefinitions()`'s sibling `CryptoCurrencies` map)\nonly for soft, non-authoritative UI hints (e.g. \"KYC may be required above\n$X\").\n\n## Reading state and reacting to changes\n\n```ts\n// On-chain activity state (only present after getUserState()):\nconst bc = client.data.user.state?.Blockchain;\nbc?.LinkedWallets; // Record<networkID, LinkedWalletInfo>\nbc?.PendingWithdrawals; // PendingWithdrawalRef[] — light refs, not full tx docs\nbc?.Kyc; // UserKycState\nbc?.Stats; // BlockchainStats (deposit/withdrawal counters & volume)\n\n// Crypto balances (decimal-as-string), same cache Currency module reads:\nclient.data.user.getCryptoCurrencyAmount(\"usdt\");\n\n// Definitions (cached after getDefinitions()):\nimport type { BlockchainDefinitions } from \"@idosgames/core\";\nconst defs = client.data.config.getSection<BlockchainDefinitions>(\"Blockchain\");\n```\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\n\n- `blockchain:definitionsLoaded` → `BlockchainConfigResponse`\n- `blockchain:userStateLoaded` → `UserBlockchainStateResponse`\n- `blockchain:tokenDeposited` → `DepositTokenResponse`\n- `blockchain:nftDeposited` → `DepositNFTResponse`\n- `blockchain:tokenWithdrawalRequested` → `TokenWithdrawalResponse`\n- `blockchain:nftWithdrawalRequested` → `NFTWithdrawalResponse`\n- `blockchain:transactionHistoryLoaded` → `TransactionHistoryResponse`\n- `blockchain:withdrawalRetried` → `RetryWithdrawalResponse`\n- `blockchain:withdrawalConfirmed` → `ConfirmWithdrawalResponse`\n- `blockchain:donatedToDeveloper` → `DonationResponse`\n- `blockchain:donatedToUsersPool` → `DonationResponse`\n\n**Cache writes are not uniform across this module — read this carefully:**\n\n- `getUserState()` is the only call that writes `client.data.user.state.Blockchain`\n (`LinkedWallets`, `PendingWithdrawals`, `Kyc`, `Stats`) and fires the coarse\n `user:blockchainUpdated` (+ `user:anyUpdated`).\n- `depositToken` / `requestTokenWithdrawal` patch only the crypto **balance**\n (`InventoryV2.CryptoCurrencies`) via a decimal delta, firing\n `user:inventoryUpdated` (+ `user:anyUpdated`) — **not** `user:blockchainUpdated`.\n- `depositNFT` / `requestNFTWithdrawal` patch inventory (items and/or\n currencies) via the shared `Resources` resource-operation pipeline, firing\n `user:inventoryUpdated` (and `user:virtualCurrencyUpdated` if VC moved) —\n again **not** `user:blockchainUpdated`.\n- `getTransactionHistory`, `retryWithdrawal`, `confirmWithdrawal`,\n `donateToDeveloper`, `donateToUsersPool` only emit their own\n `blockchain:*` event — they don't touch `client.data.user.state` at all.\n\nPractical consequence: after a deposit or withdrawal request, your **balance**\nis fresh in the cache, but `client.data.user.state.Blockchain.PendingWithdrawals`\nand `.Stats` are stale until you call `getUserState()` again. Re-fetch\n`getUserState()` after a withdrawal request/confirm/retry if your UI shows the\npending-withdrawals list or stats.\n\n**`StateDelta` / `Inventory` — the response already carries what changed, if\nyou want to apply it yourself instead of re-fetching.** `DepositTokenResponse`,\n`TokenWithdrawalResponse`, `NFTWithdrawalResponse`, and\n`ConfirmWithdrawalResponse` all carry an optional `StateDelta`\n(`BlockchainStateDelta`): a signed `CryptoBalances` delta per currency\n(`{ AmountDelta, FrozenDelta, UpdatedAt }` — add, don't overwrite), a\n`PendingAdded` ref (this call's newly-added pending withdrawal, if any), and\n`PendingRemovedIDs` (pending withdrawals this call confirmed or lazily\nexpired). `DepositNFTResponse` / `NFTWithdrawalResponse` similarly carry an\n`Inventory` (`InventoryDelta`) for the NFT's `UnstackableItems` instance —\nsame shape/semantics as the character-system module's `Inventory` deltas\n(`ChangedInstances` to upsert, `RemovedInstanceIDs` to drop). This mirrors the\nself-sufficient-response pattern used elsewhere in this SDK (see the\ncharacter-system skill) so a client that wants to reconcile\n`PendingWithdrawals`/balances/instances without another round trip can do so\nstraight from the mutating call's response. **Note:** `BlockchainService`\nitself does not auto-apply `StateDelta`/`Inventory` into\n`client.data.user.state.Blockchain` today — only the crypto **balance**\n(via the existing `AmountNative`-based patch) and item `Resources` are\napplied automatically. If you need `PendingWithdrawals` reconciled without a\nfull `getUserState()` refetch, read `result.data.StateDelta` yourself. Both\nare `null`/absent on an idempotent replay (nothing new to apply).\n\n```ts\nconst off = client.on(\"blockchain:tokenWithdrawalRequested\", (r) => {\n console.log(`Withdrawal ${r.TitleTransactionID} expires at ${r.ExpiresAt}`);\n});\n// later: off();\n```\n\n## Recipes\n\n### Load config + state on a wallet/blockchain screen\n\n```ts\nawait client.blockchain.getDefinitions();\nawait client.blockchain.getUserState();\n\nconst defs = client.data.config.getSection<BlockchainDefinitions>(\"Blockchain\");\nconst bc = client.data.user.state?.Blockchain;\n\nfor (const [networkID, net] of Object.entries(defs?.Networks ?? {})) {\n if (!net.DepositsEnabled && !net.WithdrawalsEnabled) continue;\n // render a network card; net.NftCollections binds contracts to item catalogs\n}\nbc?.Kyc?.Status; // gate withdrawal UI on KYC if the title requires it\n```\n\n### Deposit a token (player already sent it on-chain)\n\n```ts\nconst res = await client.blockchain.depositToken(\"polygon\", \"0xabc123...\");\nif (!res.ok) return showError(res.error); // e.g. \"Transaction not found on chain.\",\n// \"Not enough confirmations (required 12). Try again in a few minutes.\",\n// \"Transaction hash already used.\"\n\nres.data.CurrencyID; // e.g. \"usdt\"\nres.data.AmountNative; // decimal string credited\n// Balance is already updated in the cache:\nclient.data.user.getCryptoCurrencyAmount(res.data.CurrencyID!);\n```\n\n### Full withdrawal lifecycle: request -> submit on-chain -> confirm, with a retry-after-failure path\n\n```ts\n// 1. Request the withdrawal — debits in-game immediately, returns a signature payload.\nconst req = await client.blockchain.requestTokenWithdrawal(\n \"usdt\",\n \"polygon\",\n \"0xPlayerWallet...\",\n \"25.00\",\n);\nif (!req.ok) return showError(req.error); // e.g. \"Not enough balance: have 10 usdt, need 25.00.\",\n// \"KYC verification required for withdrawals above 1000 USD.\",\n// \"Title users-withdrawable limit reached: available 5 usdt, requested 25.00.\"\n// — see \"Withdrawal gates\" above for the full list.\n\nconst { TitleTransactionID, EvmSignature, ExpiresAt } = req.data;\n// Balance is already debited (gross amount) in the cache.\n\n// 2. Hand EvmSignature (or SolanaSignature on a Solana network) to the\n// player's wallet SDK to submit the on-chain transaction yourself —\n// this SDK does not sign/broadcast. That step can fail (rejected in\n// wallet, gas issue).\n\n// 2a. If on-chain submission failed WHILE the transaction is still Pending\n// (before ExpiresAt), retry — this re-issues a fresh signature WITHOUT\n// debiting again:\nconst retry = await client.blockchain.retryWithdrawal(TitleTransactionID!);\nif (!retry.ok) return showError(retry.error); // e.g. \"Transaction is not in Pending state (current: Abandoned).\"\nconst freshSignature = retry.data.EvmSignature ?? retry.data.SolanaSignature;\n// Submit freshSignature on-chain instead, then continue to step 3.\n//\n// IMPORTANT: retryWithdrawal only works while the transaction is Pending. If\n// ExpiresAt already passed, the backend has lazily moved it to Abandoned and\n// retryWithdrawal will reject it — there is no \"re-request\" for an Abandoned\n// withdrawal (the asset was already debited and is not refunded). The only\n// way to still complete it is confirmWithdrawal with a hash, if the player\n// actually managed to submit the original signature before it was swept —\n// see the Gotchas section.\n\n// 3. Once the wallet actually broadcasts the transaction, tell the backend\n// the resulting on-chain hash so it can verify and close out the withdrawal:\nconst confirm = await client.blockchain.confirmWithdrawal(\n TitleTransactionID!,\n \"0xOnChainTxHash...\",\n);\nif (!confirm.ok) return showError(confirm.error);\nconfirm.data.Status; // e.g. \"Completed\" once the chain confirms it\n\n// 4. Refresh state — request/retry/confirm don't touch Blockchain cache themselves.\nawait client.blockchain.getUserState();\nclient.data.user.state?.Blockchain?.PendingWithdrawals; // should no longer list it once Completed\n```\n\n### KYC-gated withdrawal\n\nThe client never decides whether KYC is required — the backend compares the\nwithdrawal's USD-equivalent against the currency's configured threshold at\nrequest time. Use `Kyc.Status` only to pre-empt an obvious rejection in the\nUI; still branch on the real error:\n\n```ts\nawait client.blockchain.getUserState();\nconst kyc = client.data.user.state?.Blockchain?.Kyc;\n\nif (kyc?.Status !== \"Verified\") {\n // Optional UX nicety: warn before the call for large amounts. This SDK has\n // no startKyc/submitKyc method — verification happens through whatever KYC\n // provider integration the title uses outside this SDK; Kyc here only\n // reflects the result.\n}\n\nconst req = await client.blockchain.requestTokenWithdrawal(\n \"usdt\",\n \"polygon\",\n \"0xPlayerWallet...\",\n \"5000.00\",\n);\nif (!req.ok) {\n if (req.error.startsWith(\"KYC verification required\")) {\n // Route the player to the title's KYC verification flow.\n }\n return showError(req.error);\n}\n```\n\n### Deposit / withdraw an NFT\n\n```ts\n// Deposit: player already transferred the NFT to the vault address on-chain.\nconst dep = await client.blockchain.depositNFT(\"ethereum\", \"0xNftDepositTx...\");\nif (!dep.ok) return showError(dep.error);\ndep.data.ItemID; // the in-game item granted\ndep.data.Resources; // already applied to inventory in the cache\n\n// Withdraw: consumes the in-game item, returns a signature to submit on-chain.\nconst wd = await client.blockchain.requestNFTWithdrawal(\n \"sword-of-embers-inst-1\", // ItemID (per NFTWithdrawalResponse/BlockchainRequest shape)\n \"ethereum\",\n \"0xPlayerWallet...\",\n \"1\",\n);\nif (!wd.ok) return showError(wd.error);\nwd.data.TitleTransactionID; // use with retryWithdrawal / confirmWithdrawal exactly as tokens above\n\n// ERC-721 unique NFT: pass the specific instance to tokenize. level/itemInstanceID\n// are the trailing optional args — see the Methods table above.\nconst wd721 = await client.blockchain.requestNFTWithdrawal(\n \"sword-of-embers\",\n \"ethereum\",\n \"0xPlayerWallet...\",\n \"1\",\n undefined, // category\n 1, // level\n \"sword-of-embers-inst-1\", // ItemInstanceID — required for ERC-721 bindings\n);\n```\n\n### Edge case: not logged in / missing args\n\n```ts\nconst res = await client.blockchain.depositToken(\"\", \"0xabc\");\n// res.ok === false, res.reason === \"client\" — \"NetworkID is required.\" — no network call.\n\nconst res2 = await client.blockchain.getUserState();\n// If called before auth.loginWithDeviceID() (or any auth.* login):\n// res2.ok === false, res2.reason === \"unauthorized\"\n```\n\n### Donate crypto (no personal credit)\n\n```ts\nconst res = await client.blockchain.donateToDeveloper(\n \"polygon\",\n \"0xdonateTx...\",\n);\nif (!res.ok) return showError(res.error);\nres.data.Target; // \"Developer\" — confirms which pool bucket it landed in\n\n// donateToUsersPool is identical in shape, credits the users' pool bucket instead:\nawait client.blockchain.donateToUsersPool(\"polygon\", \"0xdonateTx2...\");\n```\n\n## Gotchas\n\n- **Withdrawal request debits immediately; the on-chain leg is separate and\n can fail.** `requestTokenWithdrawal`/`requestNFTWithdrawal` already took the\n asset from the player before any on-chain transaction exists. If the\n player's wallet fails to submit (rejected, gas issue) **while the\n transaction is still `Pending`**, don't ask them to request again — that\n would debit twice. Use `retryWithdrawal` with the same\n `TitleTransactionID` to get a fresh signature without a new charge. This\n only works before `ExpiresAt` — see the next two points for what happens\n after.\n- **`retryWithdrawal` vs `confirmWithdrawal` are opposite ends of the same\n flow.** Retry re-issues the _signed payload_ before submission (nothing has\n reached the chain yet); confirm reports the _resulting tx hash_ after\n submission (the chain now has it). Calling confirm with a hash from a\n transaction that never actually landed on-chain will simply fail\n server-side verification — don't fabricate a hash to \"force\" completion.\n- **`ExpiresAt` is real, expiry does not refund the player, and — contrary to\n what the name suggests — an expired withdrawal is NOT retryable.** A\n withdrawal signature is time-boxed (`TokenWithdrawalResponse.ExpiresAt` /\n `NFTWithdrawalResponse.ExpiresAt`, driven by\n `BlockchainAccountSafetyPolicy.PendingWithdrawalTtlHours`). Once it passes\n without a submission, the backend lazily transitions the transaction to\n **`Abandoned`** (not `Expired` — that enum value exists but this backend\n path never assigns it) and drops it off `PendingWithdrawals` — but the\n already-debited asset is **not** credited back; this is intentional, not a\n bug. Critically, `retryWithdrawal` requires the transaction to still be\n `Pending` — calling it on an `Abandoned` one fails with `\"Transaction is\nnot in Pending state (current: Abandoned).\"` There is no \"re-request\"\n operation for an abandoned withdrawal.\n- **A withdrawal can still be confirmed after it's `Abandoned`.** If the\n player submits late — after `ExpiresAt` passed and the backend already\n swept it to `Abandoned` — `confirmWithdrawal` still accepts it as long as\n the on-chain transaction verifies (the signature itself doesn't expire\n on-chain, only the title's own bookkeeping window does). Don't treat an\n `Abandoned` transaction as unrecoverable if the player insists they\n submitted it; calling `confirmWithdrawal` with the resulting hash is still\n the right move, and is in fact the _only_ way to close out an\n already-expired-but-actually-submitted withdrawal.\n- **`retryWithdrawal` only works on a `Pending` transaction the caller owns.**\n It fails with `\"Transaction not found.\"` for an unknown or someone else's\n `TitleTransactionID`, `\"Transaction is not in Pending state (current:\n{status}).\"` if it already completed/failed/was abandoned, or\n `\"Signature data not found for this transaction.\"` if there's nothing to\n reissue. A banned account additionally gets `\"Account is banned. Contact\nsupport.\"` on retry (deposits stay allowed for banned accounts; retrying a\n withdrawal does not).\n- **Gross vs. net amounts on token withdrawals.** `AmountNative` is what was\n debited from the player (gross); `NetAmountNative` is what actually gets\n paid out on-chain after a platform commission percentage **and** an\n optional on-chain burn are deducted (`NetAmountNative = AmountNative −\ncommission − BurnAmountNative`). Show the player the net figure they'll\n receive, not the gross debit, to avoid support tickets about a \"missing\"\n amount. NFT withdrawals have no such split — there's no `NetAmountNative`\n on `NFTWithdrawalResponse`.\n- **Burn on withdrawal (EVM-only).** `TokenWithdrawalResponse.BurnAmountNative`\n is the amount burned on-chain (sent to the DEAD address) for this\n withdrawal, driven by the currency's `WithdrawalBurnPercent` (see the\n currency-system skill) — `0` if burn is disabled for that currency or the\n network is Solana. The raw-units counterpart, `WithdrawalSignatureResponse.\nBurnAmount`, is bound into the signed hash and must be passed to the\n contract call verbatim, same as `Amount`/`Nonce` — `@idosgames/wallet`'s\n `submitEvmTokenWithdrawal` does this for you; a client calling\n `withdrawERC20` directly must include it too, or the signature check fails.\n- **`client.data.user.state.Blockchain` goes stale after deposits/withdrawal\n requests.** Only `getUserState()` refreshes `LinkedWallets`,\n `PendingWithdrawals`, `Kyc`, and `Stats`. A deposit/withdrawal call updates\n your _balance_/_inventory_ cache correctly, but if your UI also shows the\n pending-withdrawals list or lifetime stats, re-call `getUserState()`\n afterward (see the withdrawal recipe above).\n- **Deposits are reporting, not sending.** `depositToken`/`depositNFT` don't\n move any asset on-chain — they tell the backend \"verify this transaction\n hash and credit me.\" The actual on-chain transfer to the platform's pool/\n vault address must already have happened via a wallet SDK before you call\n these.\n- **This SDK never signs or broadcasts.** `EvmSignature`/`SolanaSignature`\n payloads are inputs to a wallet SDK/contract call that happens outside\n `@idosgames/core`. Don't look for a \"submit on-chain\" method here — there\n isn't one; `confirmWithdrawal` only reports the result afterward. The\n `@idosgames/wallet` companion package is that outside layer — it submits the\n signature on-chain and calls `confirmWithdrawal` for you.\n- **Donations never touch personal balances.** `donateToDeveloper` /\n `donateToUsersPool` intentionally don't credit the player anything and\n don't touch `client.data.user.state` — they only emit their own\n `blockchain:donated*` event for a confirmation toast/receipt.\n- **Render from the cache, handle the error from the result.** The happy\n path updates the relevant cache slice + emits an event; the failure path\n gives you `reason` + `error`. Use `reason` to decide behavior (retry on\n `\"connection\"`, re-auth on `\"unauthorized\"`, toast the `error` on\n `\"server\"`).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — every config and\nstate field: network definitions, NFT collection bindings, account-safety\npolicy, KYC state, transaction documents, the withdrawal-gate limits, and the\nEVM/Solana withdrawal signature payload shapes. Read it when building network\npickers, a transaction-history table, or KYC/limit-aware withdrawal UI. For\nthe shared `ResourceConsume`/`ResourceGrant`/`ResourceOperation`\ncost-and-reward shapes riding along on `depositNFT`/`requestNFTWithdrawal`,\nand for the full `CryptoCurrencyDefinition` shape (`Limits`, `Networks[].\nMinWithdraw`, `Permissions`) that the withdrawal gates enforce,\nsee the currency-system skill.\n",
4
+ "content": "---\nname: blockchain-system\ndescription: >-\n Bridge in-game assets on-chain in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.blockchain (BlockchainService): load blockchain\n network/config definitions, load the player's on-chain state (linked\n wallets, pending withdrawals, KYC, stats), deposit a token or NFT from a\n wallet into the game, request a token or NFT withdrawal out to a wallet,\n read on-chain transaction history, retry a still-pending withdrawal's\n signature, confirm a withdrawal's on-chain tx hash, and donate crypto to\n the developer or a users' pool. Use this whenever the user is working in\n the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants crypto wallets, NFT\n deposits/withdrawals, token bridging, on-chain asset transfers, KYC status,\n or otherwise touches client.blockchain, BlockchainService,\n BlockchainDefinitions, UserBlockchainState, DepositTokenResponse,\n TokenWithdrawalResponse, or NFTWithdrawalResponse — even if they don't name\n the module explicitly.\n---\n\n# Blockchain system (iDosGames TS SDK)\n\nThe Blockchain module bridges in-game assets to and from real wallets on\nsupported chains (EVM and Solana networks). A player can deposit a token or\nNFT they already sent on-chain (crediting their in-game balance/inventory),\nor request a withdrawal that pays an in-game token/NFT out to their wallet\n(debiting their in-game balance/inventory and producing a signature the\nplayer submits on-chain themselves). Everything is **server-authoritative**:\nthe client reports/requests, the backend validates the transaction against\nthe chain, applies rules (network enabled, KYC, account-safety policy), and\nthe SDK mirrors the confirmed result into a local cache your UI reads. You\nnever credit/debit balances yourself.\n\nThis skill is for **using** the production `BlockchainService`, not for\nporting or extending it, and not for signing/broadcasting transactions\nyourself — this SDK reports deposits and requests withdrawals; actually\nsending the on-chain transaction (the deposit transfer, or broadcasting a\nwithdrawal signature) happens with a wallet SDK outside this client.\n\n> **The on-chain half is the `@idosgames/wallet` companion package.** It\n> connects browser & mobile wallets (EVM via wagmi/viem/WalletConnect, Solana\n> via wallet-adapter) and runs the exact RewardPool contract calls, threading\n> them through this service's request → submit → confirm / approve → deposit →\n> report lifecycle. If the user wants to actually connect a wallet and move\n> tokens/NFTs (not just call `client.blockchain.*`), reach for that package —\n> see its README. Everything below documents the server-authoritative\n> `client.blockchain` surface that `@idosgames/wallet` builds on. The wallet\n> package's EVM surface covers both NFT standards: `submitEvmNftWithdrawal` /\n> `depositNftEvm` (+ `erc1155Abi`) for ERC-1155 collections, and\n> `submitEvmNftWithdrawal721` / `depositNftEvm721` (+ `erc721Abi`) for\n> ERC-721 unique-item collections (4-arg `safeTransferFrom`, no `id`/`amount`\n> — the token is always qty 1). `submitEvmTokenWithdrawal`'s `withdrawERC20`\n> call now also threads `sig.BurnAmount` through (see\n> [Burn on withdrawal](#gotchas) below) — the ABI/contract call order changed,\n> so an app pinned to an older `@idosgames/wallet` build will revert on-chain\n> against an updated RewardPool contract.\n\n> **Never import `@idosgames/wallet/react` (or `/react/solana`) from a file\n> that loads on startup.** Those subpaths pull in Reown AppKit, and AppKit is\n> deliberately _not_ a dependency of a generated project — the live preview\n> resolves every declared dependency up front and times out on AppKit's tree.\n> A static import therefore blanks the preview before any game code runs\n> (`Could not find dependency: '@reown/appkit-adapter-wagmi'`), while the real\n> build stays green — that mismatch is the signature of this mistake.\n> Two rules keep both working:\n>\n> - **Sign-in button:** import `LazyWalletLogin` / `LazySolanaWalletLogin` from\n> `@idosgames/wallet/react/lazy` (that entry has no AppKit in its graph; it\n> also re-exports the chains as plain objects — never import chains from\n> `wagmi/chains` or `viem/chains`, that barrel breaks the preview too).\n> - **In-game deposit/withdraw panel:** import `LazyWalletPanel` from\n> `@idosgames/wallet/react/lazy` and pass it the authenticated `client` (a\n> prop, like the login button — never a module context). Same lazy contract:\n> AppKit stays out of the startup graph. Don't hand-roll your own\n> `await import(\"./PanelImpl\")` wrapper — `LazyWalletPanel` is that wrapper.\n> Because the wallet config is memoised per WalletConnect project id, the\n> panel reuses the wallet the player connected at sign-in: same store, so it\n> opens already-connected, no second tap and no second modal. This is exactly\n> how the board-game and idle-rpg modules wire their `WalletPanel.tsx`.\n> ONE panel for EVM and Solana: it reads the title's networks and loads the\n> right one — same form, deposit and withdraw on both (Solana through the\n> platform pool, `createPlatformPoolAdapter`). The base already puts it in the\n> lobby header (`src/base/lobby/WalletButton.tsx`) — don't add a second one.\n\n> **The game cannot reach the wallet while it is embedded in an idosgames.com iframe** (owner's\n> decision, 2026-09-17 — deposit/withdraw goes through the buttons in the site's own wallet card).\n> A wallet BUTTON inside the frame calls `openPlatformWalletPanel()` (from `@idosgames/wallet/react/lazy`)\n> — the site shows its own wallet card; that is what the base's lobby button does.\n> `LazyWalletPanel` itself renders nothing there, and\n> `LazyWalletLogin` / `LazySolanaWalletLogin` stay a working \"Sign in with wallet\" button — inside\n> the frame the SITE's connected wallet signs the engine's login message for the game\n> (`loginWithWalletViaPlatform` from `@idosgames/wallet`, used by the buttons automatically). Never\n> show a \"use idosgames.com\" notice inside the game. Every flow function that talks to the wallet\n> directly (`depositTokenEvm`, `withdrawTokenEvm`, `loginWithWalletEvm`, their Solana equivalents,\n> `payWithWalletEvm`) refuses with `WALLET_USE_SITE_PANEL` inside a frame. A game opened by its own\n> direct link (not embedded) is unaffected — every one of these works exactly as before there.\n\n### Login mode and play access by token balance (`cfg.Blockchain.PlayAccess`)\n\nA title can require players to HOLD tokens to play. The publisher picks one of three login modes:\n\n| `Mode` | Who can sign in | Balance check |\n| --------------------- | ----------------------------- | -------------------------------------------- |\n| `Open` (default) | every method, guests included | none |\n| `AnyWithLinkedWallet` | every method except guest | the wallet the player linked by signature |\n| `WalletOnly` | wallet login only | the wallet they signed in with — and only it |\n\nIn `WalletOnly` the account must BE the wallet's account: an account created another way before the\nmode was switched on gets `PLAY_ACCESS_WALLET_LOGIN_ONLY` on every check, and `linkWallet` refuses\nany wallet but the account's own with the same code — don't offer \"link wallet\" there, offer\n\"log out and sign in with your wallet\".\n\nRequirements are `TokenGates` (`CurrencyID`, `MinBalance`, `BalanceSource`: `OnChain` wallet /\n`InGame` game balance — no wallet needed / `Combined`; `NetworkID` for wallet ones). The SERVER\nchecks the balance on every login and session refresh — silently, no signature — and on\n`client.blockchain.confirmPlayAccess()`. Gameplay calls without a valid pass fail with HTTP 403\n`PLAY_ACCESS_REQUIRED`; `Blockchain` itself, account linking and the login-time `User` reads stay open.\n\n**The host template already handles all of it — don't rebuild it.** `mountHost` shows the\nplay-access screen (`renderPlayAccess`, the template's `src/PlayAccessScreen.tsx` over\n`DefaultPlayAccessScreen`) instead of the game whenever `client.auth.playAccess?.Granted === false`\nor `playAccess:required` fires, and the template's login screen hides the methods the mode refuses\n(`client.auth.getAuthOptions()` → `PlayAccessMode`). \"Link wallet\" on that screen is\n`LazyWalletLogin mode=\"link\"` (`renderWalletLink` in `src/walletLogin.tsx`); inside the\nidosgames.com frame the site's wallet signs the link (`linkWalletViaPlatform`).\n\nClient surface, if you roll your own screen: `client.auth.playAccess` (+ `playAccess:changed`),\n`client.blockchain.getPlayAccess()` (mode, requirements, the player's signature-linked wallets, last\ncheck), `client.blockchain.confirmPlayAccess(networkID?)` (choose a linked wallet and check now),\n`client.auth.linkWallet(address, networkID, signature?)` (two steps; the second returns the pass\nchecked against that wallet), `linkWalletEvm` / `linkWalletSolana` / `linkWalletViaPlatform` in\n`@idosgames/wallet`. Refusal codes: `PLAY_ACCESS_WALLET_REQUIRED`,\n`PLAY_ACCESS_WALLET_NETWORK_NOT_GATED`, `PLAY_ACCESS_GUEST_DISABLED`, `PLAY_ACCESS_WALLET_LOGIN_ONLY`,\n`PLAY_ACCESS_CHECK_FAILED` (the RPC could not be read — NOT an empty wallet; check again), or the\nrequirement's own `DenyMessage`. A login or refresh never fails for lack of balance — in every mode\nthe session is issued and the game stays closed until the pass is granted.\n\n### Operation category (`game_topup` by default)\n\nThe updated RewardPool contract tags every deposit/withdrawal with a string\n**category** (default `\"game_topup\"`; `\"community_reward\"` is the other known\nvalue, and arbitrary strings are allowed). On a **withdrawal** the server signs\nthe category into the on-chain hash and returns it on the signature payload\n(`EvmSignature.Category` / `.TitleID`); whoever submits the transaction **must\npass the same value on-chain verbatim** or the contract rejects the signature —\n`@idosgames/wallet` does this for you. Pass it as the optional last arg to\n`requestTokenWithdrawal`/`requestNFTWithdrawal` (omit → `\"game_topup\"`). On a\n**deposit** the category is read back from the on-chain transaction, so you\ndon't pass it to `depositToken`/`depositNFT`. It also appears on transaction\ndocuments as `Category`. Import the constants from `@idosgames/core`:\n`BlockchainOperationCategory.GameTopUp` / `.CommunityReward`.\n\n## Mental model: deposits vs. withdrawals\n\n- **Deposit** = the player already sent tokens/an NFT to the platform's pool\n or vault address on-chain. The client then calls `depositToken`/`depositNFT`\n with that transaction's hash so the backend can verify it and credit the\n player in-game. One-shot: the credit happens directly on a successful call.\n- **Withdrawal** = the player wants an in-game token/NFT sent out to their\n wallet. The client calls `requestTokenWithdrawal`/`requestNFTWithdrawal`,\n which **debits in-game immediately** and returns a signed payload\n (`EvmSignature` or `SolanaSignature`) the player's wallet must submit\n on-chain to actually receive the asset. This is a **multi-step, async\n flow** — see [Recipes](#recipes) for the full lifecycle, including what to\n do when the on-chain submission fails.\n\nBoth flows are per-network: every call takes a `networkID` that must match one\nof the title's configured `Networks` (EVM or Solana), each with its own\ndeposit/withdrawal enable flags, contract/vault addresses, and (for NFTs) a\ncollection binding to an item catalog. Withdrawals additionally run through a\nlong chain of server-side gates (balances, per-network minimums, account\nsafety, KYC, daily/monthly compliance limits, a collective title-wide pool\ncap, and platform commission) — see\n[Withdrawal gates](#withdrawal-gates-what-can-reject-a-request) for the full\nlist with verbatim error strings.\n\nFor the full config/state field shapes (network definitions, NFT collection\nbindings, KYC tiers, transaction documents, signature payloads), read\n[references/data-model.md](references/data-model.md). You do **not** need it\nto call the methods — only to drive richer UI (network pickers, KYC gates,\ntransaction history tables).\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any auth.* method\n\nconst blockchain = client.blockchain; // the BlockchainService\n```\n\nEvery blockchain method requires an authenticated session. Without one they\nreturn `{ ok: false, reason: \"unauthorized\" }` — they do not throw.\n\n## Methods\n\nAll methods return `Promise<OperationResult<T>>`: a discriminated union that\nis either `{ ok: true, data }` or `{ ok: false, reason, error }`. Always\nbranch on `result.ok` before touching `result.data`. `reason` is one of\n`\"client\"` (missing/empty required arg — rejected before any network call),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\n600ms client-side throttle window), `\"connection\"` (transient, offer Retry),\n`\"validation\"` (response/schema drift), or `\"server\"` (backend rejected it —\n`error` carries the exact backend message). Withdrawals in particular can be\nrejected by a long chain of server-side gates — see\n[Withdrawal gates](#withdrawal-gates-what-can-reject-a-request) below for the\nfull list with verbatim error strings.\n\n| Method | Purpose | `data` on success |\n| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------- |\n| `getDefinitions()` | Load the title's blockchain config: networks, NFT bindings, crypto currencies. | `BlockchainConfigResponse` |\n| `getUserState()` | Load this player's on-chain state: linked wallets, pending withdrawals, KYC, stats, crypto balances. | `UserBlockchainStateResponse` |\n| `depositToken(networkID, transactionHash)` | Report an on-chain token transfer; credits the matching crypto balance. | `DepositTokenResponse` |\n| `depositNFT(networkID, transactionHash)` | Report an on-chain NFT transfer; grants the matching in-game item. | `DepositNFTResponse` |\n| `requestTokenWithdrawal(currencyID, networkID, walletAddress, amount, category?)` | Debit a crypto balance and get a signed payload to withdraw on-chain. | `TokenWithdrawalResponse` |\n| `requestNFTWithdrawal(itemID, networkID, walletAddress, amount, category?, level?, itemInstanceID?)` | Consume an in-game item and get a signed payload to withdraw the NFT on-chain. | `NFTWithdrawalResponse` |\n| `getTransactionHistory(limit?)` | Load recent token + NFT transaction documents (default limit 50). | `TransactionHistoryResponse` |\n| `retryWithdrawal(titleTransactionID)` | Re-issue a fresh signature for a still-`Pending` withdrawal without re-debiting. | `RetryWithdrawalResponse` |\n| `confirmWithdrawal(titleTransactionID, onChainTransactionHash)` | Tell the backend the signed withdrawal was submitted on-chain, with its tx hash. | `ConfirmWithdrawalResponse` |\n| `donateToDeveloper(networkID, transactionHash)` | Report an on-chain transfer as a donation to the developer pool (no personal credit). | `DonationResponse` |\n| `donateToUsersPool(networkID, transactionHash)` | Report an on-chain transfer as a donation to the users' pool (no personal credit). | `DonationResponse` |\n\nAll string args (`networkID`, `transactionHash`, `currencyID`,\n`walletAddress`, `amount`, `itemID`, `titleTransactionID`,\n`onChainTransactionHash`) are required and checked client-side before any\nnetwork call — an empty one short-circuits with `reason: \"client\"`. `amount`\nis a decimal string for token withdrawals and an integer-as-string for NFT\nwithdrawals / not used for deposits (deposit amounts come from the verified\non-chain transaction, not from the client). `getTransactionHistory(limit)`\ndefaults to `50`, is capped at **200** server-side (values above are silently\nclamped, values `<= 0` fall back to 50), and is sent as `Amount` on the wire\n(reused request field, not an actual currency amount).\n\n`requestNFTWithdrawal`'s trailing `level`/`itemInstanceID` are both optional\nand only matter for NFT catalogs with leveled or unique (ERC-721) bindings:\n`level` selects which leveled instance/tokenId to withdraw (omit or `1` →\nbase level, prior behavior); `itemInstanceID` is **required** when the\nitem's NFT binding is ERC-721 — it tells the server exactly which\nunstackable instance to debit and tokenize (preserving its Level/RemainingUses/\nCustomData in the on-chain registry via a separate ItemBridge contract).\nBoth are ignored for stackable/ERC-1155 items.\n\nOn success, most methods **mirror the confirmed change into the cache and\nemit an event** — see the next section for exactly which cache each method\ntouches, since it's not uniform across this module.\n\n## Withdrawal gates (what can reject a request)\n\n`requestTokenWithdrawal` / `requestNFTWithdrawal` run through a long chain of\nserver-side checks, each a `reason: \"server\"` failure with a specific `error`\nstring. Surface the string; don't try to pre-validate all of these\nclient-side — the gate list can change without a client update:\n\n- **Global kill switch** — withdrawals can be turned off platform-wide\n independently of any per-network/per-currency flag: `\"Withdrawals are\ncurrently disabled.\"`\n- **Network / currency / binding disabled** — `\"Withdrawals disabled for this\nnetwork.\"`, `\"Withdrawals are disabled for currency '{id}'.\"`, `\"This\ncurrency cannot be withdrawn in this network.\"`, or (currency under\n maintenance) `\"Currency '{id}' is under maintenance. Try again later.\"`\n- **Per-network minimum** — every currency has a `MinWithdraw` for each\n network it's bound to (`CryptoNetworkBinding.MinWithdraw`, see the\n currency-system skill's data-model for the full shape): `\"Minimum withdraw\nis {MinWithdraw} {currencyID}.\"`\n- **Insufficient balance** — `\"Not enough balance: have {available}\n{currencyID}, need {amount}.\"` (tokens) or `\"Not enough items: have {owned},\nneed {amount}.\"` (NFTs).\n- **Account safety** (`BlockchainAccountSafetyPolicy`, read-only in\n `getDefinitions()`'s `AccountSafety` block) — applies to withdrawals only,\n never deposits: account younger than `MinAccountAgeDays` →\n `\"Account is too fresh. Try again later.\"`; banned account → `\"Account is\nbanned. Contact support.\"`; withdrawing to a wallet address another account\n already used, when `MultiAccountCheckEnabled` +\n `BanOnSharedWithdrawalAddress` are both on, **bans the account on the\n spot** and returns `\"Account banned. Contact support.\"`\n- **Compliance / KYC** (`CryptoCurrencyDefinition.Limits`, per currency) —\n checked in USD-equivalent of the requested amount:\n - Above `Limits.KycRequiredAboveUsd` without `Kyc.Status === \"Verified\"` →\n `\"KYC verification required for withdrawals above {threshold} USD.\"`\n - This UTC calendar day's spend would exceed `Limits.DailyWithdrawUsd`\n (window resets at 00:00 UTC, not a rolling 24h window) →\n `\"Daily withdraw limit exceeded ({spentSoFar} + {thisAmount} >\n{dailyLimit} USD).\"`\n - This UTC calendar month's spend would exceed `Limits.MonthlyWithdrawUsd`\n (window resets 00:00 UTC on the 1st) → `\"Monthly withdraw limit exceeded\n({spentSoFar} + {thisAmount} > {monthlyLimit} USD).\"`\n - Any `Limits` field can be absent/null, which disables that specific\n check for that currency. The daily/monthly counters live server-side on\n `UserCryptoCurrencyState.Compliance` (not exposed as its own client\n method) and reset at UTC day/month boundaries — there is no way to read\n \"USD spent so far today\" from the client ahead of a request; read it off\n a rejection's `error` string instead.\n- **Collective pool cap** — independent of the player's own balance, the\n title's whole player-withdrawable pool for that (network, currency) pair\n can be exhausted: `\"Title users-withdrawable limit reached: available\n{available} {currencyID}, requested {amount}.\"` This is a title-wide\n economic limit, not specific to one player — if you see it, don't retry\n immediately.\n- **Platform commission** — a platform-wide withdrawal commission percent can\n reduce the net payout; if it would consume the entire requested amount,\n the request is rejected outright: `\"Withdrawal amount is fully consumed by\nplatform commission.\"` Otherwise the withdrawal proceeds and\n `NetAmountNative` reflects the amount after commission (see\n [Gotchas](#gotchas)).\n\nNone of these are configurable or visible as a single \"can I withdraw right\nnow\" flag — the practical pattern is: build the request, call it, and render\n`error` on failure. Use `getDefinitions()`'s `AccountSafety` block and the\ncurrency's `Limits` (from `getDefinitions()`'s sibling `CryptoCurrencies` map)\nonly for soft, non-authoritative UI hints (e.g. \"KYC may be required above\n$X\").\n\n## Reading state and reacting to changes\n\n```ts\n// On-chain activity state (only present after getUserState()):\nconst bc = client.data.user.state?.Blockchain;\nbc?.LinkedWallets; // Record<networkID, LinkedWalletInfo>\nbc?.PendingWithdrawals; // PendingWithdrawalRef[] — light refs, not full tx docs\nbc?.Kyc; // UserKycState\nbc?.Stats; // BlockchainStats (deposit/withdrawal counters & volume)\n\n// Crypto balances (decimal-as-string), same cache Currency module reads:\nclient.data.user.getCryptoCurrencyAmount(\"usdt\");\n\n// Definitions (cached after getDefinitions()):\nimport type { BlockchainDefinitions } from \"@idosgames/core\";\nconst defs = client.data.config.getSection<BlockchainDefinitions>(\"Blockchain\");\n```\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\n\n- `blockchain:definitionsLoaded` → `BlockchainConfigResponse`\n- `blockchain:userStateLoaded` → `UserBlockchainStateResponse`\n- `blockchain:tokenDeposited` → `DepositTokenResponse`\n- `blockchain:nftDeposited` → `DepositNFTResponse`\n- `blockchain:tokenWithdrawalRequested` → `TokenWithdrawalResponse`\n- `blockchain:nftWithdrawalRequested` → `NFTWithdrawalResponse`\n- `blockchain:transactionHistoryLoaded` → `TransactionHistoryResponse`\n- `blockchain:withdrawalRetried` → `RetryWithdrawalResponse`\n- `blockchain:withdrawalConfirmed` → `ConfirmWithdrawalResponse`\n- `blockchain:donatedToDeveloper` → `DonationResponse`\n- `blockchain:donatedToUsersPool` → `DonationResponse`\n\n**Cache writes are not uniform across this module — read this carefully:**\n\n- `getUserState()` is the only call that writes `client.data.user.state.Blockchain`\n (`LinkedWallets`, `PendingWithdrawals`, `Kyc`, `Stats`) and fires the coarse\n `user:blockchainUpdated` (+ `user:anyUpdated`).\n- `depositToken` / `requestTokenWithdrawal` patch only the crypto **balance**\n (`InventoryV2.CryptoCurrencies`) via a decimal delta, firing\n `user:inventoryUpdated` (+ `user:anyUpdated`) — **not** `user:blockchainUpdated`.\n- `depositNFT` / `requestNFTWithdrawal` patch inventory (items and/or\n currencies) via the shared `Resources` resource-operation pipeline, firing\n `user:inventoryUpdated` (and `user:virtualCurrencyUpdated` if VC moved) —\n again **not** `user:blockchainUpdated`.\n- `getTransactionHistory`, `retryWithdrawal`, `confirmWithdrawal`,\n `donateToDeveloper`, `donateToUsersPool` only emit their own\n `blockchain:*` event — they don't touch `client.data.user.state` at all.\n\nPractical consequence: after a deposit or withdrawal request, your **balance**\nis fresh in the cache, but `client.data.user.state.Blockchain.PendingWithdrawals`\nand `.Stats` are stale until you call `getUserState()` again. Re-fetch\n`getUserState()` after a withdrawal request/confirm/retry if your UI shows the\npending-withdrawals list or stats.\n\n**`StateDelta` / `Inventory` — the response already carries what changed, if\nyou want to apply it yourself instead of re-fetching.** `DepositTokenResponse`,\n`TokenWithdrawalResponse`, `NFTWithdrawalResponse`, and\n`ConfirmWithdrawalResponse` all carry an optional `StateDelta`\n(`BlockchainStateDelta`): a signed `CryptoBalances` delta per currency\n(`{ AmountDelta, FrozenDelta, UpdatedAt }` — add, don't overwrite), a\n`PendingAdded` ref (this call's newly-added pending withdrawal, if any), and\n`PendingRemovedIDs` (pending withdrawals this call confirmed or lazily\nexpired). `DepositNFTResponse` / `NFTWithdrawalResponse` similarly carry an\n`Inventory` (`InventoryDelta`) for the NFT's `UnstackableItems` instance —\nsame shape/semantics as the character-system module's `Inventory` deltas\n(`ChangedInstances` to upsert, `RemovedInstanceIDs` to drop). This mirrors the\nself-sufficient-response pattern used elsewhere in this SDK (see the\ncharacter-system skill) so a client that wants to reconcile\n`PendingWithdrawals`/balances/instances without another round trip can do so\nstraight from the mutating call's response. **Note:** `BlockchainService`\nitself does not auto-apply `StateDelta`/`Inventory` into\n`client.data.user.state.Blockchain` today — only the crypto **balance**\n(via the existing `AmountNative`-based patch) and item `Resources` are\napplied automatically. If you need `PendingWithdrawals` reconciled without a\nfull `getUserState()` refetch, read `result.data.StateDelta` yourself. Both\nare `null`/absent on an idempotent replay (nothing new to apply).\n\n```ts\nconst off = client.on(\"blockchain:tokenWithdrawalRequested\", (r) => {\n console.log(`Withdrawal ${r.TitleTransactionID} expires at ${r.ExpiresAt}`);\n});\n// later: off();\n```\n\n## Recipes\n\n### Load config + state on a wallet/blockchain screen\n\n```ts\nawait client.blockchain.getDefinitions();\nawait client.blockchain.getUserState();\n\nconst defs = client.data.config.getSection<BlockchainDefinitions>(\"Blockchain\");\nconst bc = client.data.user.state?.Blockchain;\n\nfor (const [networkID, net] of Object.entries(defs?.Networks ?? {})) {\n if (!net.DepositsEnabled && !net.WithdrawalsEnabled) continue;\n // render a network card; net.NftCollections binds contracts to item catalogs\n}\nbc?.Kyc?.Status; // gate withdrawal UI on KYC if the title requires it\n```\n\n### Deposit a token (player already sent it on-chain)\n\n```ts\nconst res = await client.blockchain.depositToken(\"polygon\", \"0xabc123...\");\nif (!res.ok) return showError(res.error); // e.g. \"Transaction not found on chain.\",\n// \"Not enough confirmations (required 12). Try again in a few minutes.\",\n// \"Transaction hash already used.\"\n\nres.data.CurrencyID; // e.g. \"usdt\"\nres.data.AmountNative; // decimal string credited\n// Balance is already updated in the cache:\nclient.data.user.getCryptoCurrencyAmount(res.data.CurrencyID!);\n```\n\n### Full withdrawal lifecycle: request -> submit on-chain -> confirm, with a retry-after-failure path\n\n```ts\n// 1. Request the withdrawal — debits in-game immediately, returns a signature payload.\nconst req = await client.blockchain.requestTokenWithdrawal(\n \"usdt\",\n \"polygon\",\n \"0xPlayerWallet...\",\n \"25.00\",\n);\nif (!req.ok) return showError(req.error); // e.g. \"Not enough balance: have 10 usdt, need 25.00.\",\n// \"KYC verification required for withdrawals above 1000 USD.\",\n// \"Title users-withdrawable limit reached: available 5 usdt, requested 25.00.\"\n// — see \"Withdrawal gates\" above for the full list.\n\nconst { TitleTransactionID, EvmSignature, ExpiresAt } = req.data;\n// Balance is already debited (gross amount) in the cache.\n\n// 2. Hand EvmSignature (or SolanaSignature on a Solana network) to the\n// player's wallet SDK to submit the on-chain transaction yourself —\n// this SDK does not sign/broadcast. That step can fail (rejected in\n// wallet, gas issue).\n\n// 2a. If on-chain submission failed WHILE the transaction is still Pending\n// (before ExpiresAt), retry — this re-issues a fresh signature WITHOUT\n// debiting again:\nconst retry = await client.blockchain.retryWithdrawal(TitleTransactionID!);\nif (!retry.ok) return showError(retry.error); // e.g. \"Transaction is not in Pending state (current: Abandoned).\"\nconst freshSignature = retry.data.EvmSignature ?? retry.data.SolanaSignature;\n// Submit freshSignature on-chain instead, then continue to step 3.\n//\n// IMPORTANT: retryWithdrawal only works while the transaction is Pending. If\n// ExpiresAt already passed, the backend has lazily moved it to Abandoned and\n// retryWithdrawal will reject it — there is no \"re-request\" for an Abandoned\n// withdrawal (the asset was already debited and is not refunded). The only\n// way to still complete it is confirmWithdrawal with a hash, if the player\n// actually managed to submit the original signature before it was swept —\n// see the Gotchas section.\n\n// 3. Once the wallet actually broadcasts the transaction, tell the backend\n// the resulting on-chain hash so it can verify and close out the withdrawal:\nconst confirm = await client.blockchain.confirmWithdrawal(\n TitleTransactionID!,\n \"0xOnChainTxHash...\",\n);\nif (!confirm.ok) return showError(confirm.error);\nconfirm.data.Status; // e.g. \"Completed\" once the chain confirms it\n\n// 4. Refresh state — request/retry/confirm don't touch Blockchain cache themselves.\nawait client.blockchain.getUserState();\nclient.data.user.state?.Blockchain?.PendingWithdrawals; // should no longer list it once Completed\n```\n\n### KYC-gated withdrawal\n\nThe client never decides whether KYC is required — the backend compares the\nwithdrawal's USD-equivalent against the currency's configured threshold at\nrequest time. Use `Kyc.Status` only to pre-empt an obvious rejection in the\nUI; still branch on the real error:\n\n```ts\nawait client.blockchain.getUserState();\nconst kyc = client.data.user.state?.Blockchain?.Kyc;\n\nif (kyc?.Status !== \"Verified\") {\n // Optional UX nicety: warn before the call for large amounts. This SDK has\n // no startKyc/submitKyc method — verification happens through whatever KYC\n // provider integration the title uses outside this SDK; Kyc here only\n // reflects the result.\n}\n\nconst req = await client.blockchain.requestTokenWithdrawal(\n \"usdt\",\n \"polygon\",\n \"0xPlayerWallet...\",\n \"5000.00\",\n);\nif (!req.ok) {\n if (req.error.startsWith(\"KYC verification required\")) {\n // Route the player to the title's KYC verification flow.\n }\n return showError(req.error);\n}\n```\n\n### Deposit / withdraw an NFT\n\n```ts\n// Deposit: player already transferred the NFT to the vault address on-chain.\nconst dep = await client.blockchain.depositNFT(\"ethereum\", \"0xNftDepositTx...\");\nif (!dep.ok) return showError(dep.error);\ndep.data.ItemID; // the in-game item granted\ndep.data.Resources; // already applied to inventory in the cache\n\n// Withdraw: consumes the in-game item, returns a signature to submit on-chain.\nconst wd = await client.blockchain.requestNFTWithdrawal(\n \"sword-of-embers-inst-1\", // ItemID (per NFTWithdrawalResponse/BlockchainRequest shape)\n \"ethereum\",\n \"0xPlayerWallet...\",\n \"1\",\n);\nif (!wd.ok) return showError(wd.error);\nwd.data.TitleTransactionID; // use with retryWithdrawal / confirmWithdrawal exactly as tokens above\n\n// ERC-721 unique NFT: pass the specific instance to tokenize. level/itemInstanceID\n// are the trailing optional args — see the Methods table above.\nconst wd721 = await client.blockchain.requestNFTWithdrawal(\n \"sword-of-embers\",\n \"ethereum\",\n \"0xPlayerWallet...\",\n \"1\",\n undefined, // category\n 1, // level\n \"sword-of-embers-inst-1\", // ItemInstanceID — required for ERC-721 bindings\n);\n```\n\n### Edge case: not logged in / missing args\n\n```ts\nconst res = await client.blockchain.depositToken(\"\", \"0xabc\");\n// res.ok === false, res.reason === \"client\" — \"NetworkID is required.\" — no network call.\n\nconst res2 = await client.blockchain.getUserState();\n// If called before auth.loginWithDeviceID() (or any auth.* login):\n// res2.ok === false, res2.reason === \"unauthorized\"\n```\n\n### Donate crypto (no personal credit)\n\n```ts\nconst res = await client.blockchain.donateToDeveloper(\n \"polygon\",\n \"0xdonateTx...\",\n);\nif (!res.ok) return showError(res.error);\nres.data.Target; // \"Developer\" — confirms which pool bucket it landed in\n\n// donateToUsersPool is identical in shape, credits the users' pool bucket instead:\nawait client.blockchain.donateToUsersPool(\"polygon\", \"0xdonateTx2...\");\n```\n\n## Gotchas\n\n- **Withdrawal request debits immediately; the on-chain leg is separate and\n can fail.** `requestTokenWithdrawal`/`requestNFTWithdrawal` already took the\n asset from the player before any on-chain transaction exists. If the\n player's wallet fails to submit (rejected, gas issue) **while the\n transaction is still `Pending`**, don't ask them to request again — that\n would debit twice. Use `retryWithdrawal` with the same\n `TitleTransactionID` to get a fresh signature without a new charge. This\n only works before `ExpiresAt` — see the next two points for what happens\n after.\n- **`retryWithdrawal` vs `confirmWithdrawal` are opposite ends of the same\n flow.** Retry re-issues the _signed payload_ before submission (nothing has\n reached the chain yet); confirm reports the _resulting tx hash_ after\n submission (the chain now has it). Calling confirm with a hash from a\n transaction that never actually landed on-chain will simply fail\n server-side verification — don't fabricate a hash to \"force\" completion.\n- **`ExpiresAt` is real, expiry does not refund the player, and — contrary to\n what the name suggests — an expired withdrawal is NOT retryable.** A\n withdrawal signature is time-boxed (`TokenWithdrawalResponse.ExpiresAt` /\n `NFTWithdrawalResponse.ExpiresAt`, driven by\n `BlockchainAccountSafetyPolicy.PendingWithdrawalTtlHours`). Once it passes\n without a submission, the backend lazily transitions the transaction to\n **`Abandoned`** (not `Expired` — that enum value exists but this backend\n path never assigns it) and drops it off `PendingWithdrawals` — but the\n already-debited asset is **not** credited back; this is intentional, not a\n bug. Critically, `retryWithdrawal` requires the transaction to still be\n `Pending` — calling it on an `Abandoned` one fails with `\"Transaction is\nnot in Pending state (current: Abandoned).\"` There is no \"re-request\"\n operation for an abandoned withdrawal.\n- **A withdrawal can still be confirmed after it's `Abandoned`.** If the\n player submits late — after `ExpiresAt` passed and the backend already\n swept it to `Abandoned` — `confirmWithdrawal` still accepts it as long as\n the on-chain transaction verifies (the signature itself doesn't expire\n on-chain, only the title's own bookkeeping window does). Don't treat an\n `Abandoned` transaction as unrecoverable if the player insists they\n submitted it; calling `confirmWithdrawal` with the resulting hash is still\n the right move, and is in fact the _only_ way to close out an\n already-expired-but-actually-submitted withdrawal.\n- **`retryWithdrawal` only works on a `Pending` transaction the caller owns.**\n It fails with `\"Transaction not found.\"` for an unknown or someone else's\n `TitleTransactionID`, `\"Transaction is not in Pending state (current:\n{status}).\"` if it already completed/failed/was abandoned, or\n `\"Signature data not found for this transaction.\"` if there's nothing to\n reissue. A banned account additionally gets `\"Account is banned. Contact\nsupport.\"` on retry (deposits stay allowed for banned accounts; retrying a\n withdrawal does not).\n- **Gross vs. net amounts on token withdrawals.** `AmountNative` is what was\n debited from the player (gross); `NetAmountNative` is what actually gets\n paid out on-chain after a platform commission percentage **and** an\n optional on-chain burn are deducted (`NetAmountNative = AmountNative −\ncommission − BurnAmountNative`). Show the player the net figure they'll\n receive, not the gross debit, to avoid support tickets about a \"missing\"\n amount. NFT withdrawals have no such split — there's no `NetAmountNative`\n on `NFTWithdrawalResponse`.\n- **Burn on withdrawal (EVM-only).** `TokenWithdrawalResponse.BurnAmountNative`\n is the amount burned on-chain (sent to the DEAD address) for this\n withdrawal, driven by the currency's `WithdrawalBurnPercent` (see the\n currency-system skill) — `0` if burn is disabled for that currency or the\n network is Solana. The raw-units counterpart, `WithdrawalSignatureResponse.\nBurnAmount`, is bound into the signed hash and must be passed to the\n contract call verbatim, same as `Amount`/`Nonce` — `@idosgames/wallet`'s\n `submitEvmTokenWithdrawal` does this for you; a client calling\n `withdrawERC20` directly must include it too, or the signature check fails.\n- **`client.data.user.state.Blockchain` goes stale after deposits/withdrawal\n requests.** Only `getUserState()` refreshes `LinkedWallets`,\n `PendingWithdrawals`, `Kyc`, and `Stats`. A deposit/withdrawal call updates\n your _balance_/_inventory_ cache correctly, but if your UI also shows the\n pending-withdrawals list or lifetime stats, re-call `getUserState()`\n afterward (see the withdrawal recipe above).\n- **Deposits are reporting, not sending.** `depositToken`/`depositNFT` don't\n move any asset on-chain — they tell the backend \"verify this transaction\n hash and credit me.\" The actual on-chain transfer to the platform's pool/\n vault address must already have happened via a wallet SDK before you call\n these.\n- **This SDK never signs or broadcasts.** `EvmSignature`/`SolanaSignature`\n payloads are inputs to a wallet SDK/contract call that happens outside\n `@idosgames/core`. Don't look for a \"submit on-chain\" method here — there\n isn't one; `confirmWithdrawal` only reports the result afterward. The\n `@idosgames/wallet` companion package is that outside layer — it submits the\n signature on-chain and calls `confirmWithdrawal` for you.\n- **Donations never touch personal balances.** `donateToDeveloper` /\n `donateToUsersPool` intentionally don't credit the player anything and\n don't touch `client.data.user.state` — they only emit their own\n `blockchain:donated*` event for a confirmation toast/receipt.\n- **Render from the cache, handle the error from the result.** The happy\n path updates the relevant cache slice + emits an event; the failure path\n gives you `reason` + `error`. Use `reason` to decide behavior (retry on\n `\"connection\"`, re-auth on `\"unauthorized\"`, toast the `error` on\n `\"server\"`).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — every config and\nstate field: network definitions, NFT collection bindings, account-safety\npolicy, KYC state, transaction documents, the withdrawal-gate limits, and the\nEVM/Solana withdrawal signature payload shapes. Read it when building network\npickers, a transaction-history table, or KYC/limit-aware withdrawal UI. For\nthe shared `ResourceConsume`/`ResourceGrant`/`ResourceOperation`\ncost-and-reward shapes riding along on `depositNFT`/`requestNFTWithdrawal`,\nand for the full `CryptoCurrencyDefinition` shape (`Limits`, `Networks[].\nMinWithdraw`, `Permissions`) that the withdrawal gates enforce,\nsee the currency-system skill.\n",
5
5
  "references": [
6
6
  {
7
7
  "path": "data-model.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idosgames-compose-modules",
3
- "description": "Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards, rewards…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system, add a second game, switch between the lobby and game modes, share currency/inventory across modules, or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi, shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single module).",
4
- "content": "---\nname: idosgames-compose-modules\ndescription: >-\n Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards,\n rewards…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this\n whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system,\n add a second game, switch between the lobby and game modes, share currency/inventory across modules,\n or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi,\n shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared\n state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single\n module).\n---\n\n# Putting an app together: the base + modules\n\nEvery project is **the base + modules**. The base (`src/base/`, from the host scaffold) is sign-in and\nthe lobby; everything else is a module in `src/modules/<id>/`:\n\n- **systems** (`type: feature`, `inBase: true` in the catalog) — the shop, heroes, inventory,\n leaderboards, rewards, craft, marketplace, deals, events, season, friends, chat, invite, arena,\n profile, tutorials, workshop. Each registers its screen as a **feature** → a tab of the lobby. The\n platform installs all of them with the base into every new project; remove the ones the app does not\n need.\n- **games** (`type: template`) — board-game, idle-rpg, voxelcraft, mini-game, or the game's own. Each\n registers a **route** → a \"Play\" button in the lobby.\n\n## Register modules\n\n```ts\n// src/modules.ts — the platform regenerates it; by hand it is imports + the array\nexport const modules: Module[] = [\n storeModule, // systems: lobby tabs\n characterModule,\n leaderboardModule,\n boardGameModule, // games: \"Play\"\n idleRpgModule,\n];\n```\n\n`src/main.tsx` mounts `[baseModule, ...modules]`. After sign-in the player lands in the lobby\n(`src/app.config.ts` `start` can send them straight into a game). \"Play\" switches the host to the\ngame's mode: only the active mode's scene is mounted and ticking; the rest are suspended (their RAF\nstops). This is why two different engines (Three + Phaser) live in one project — they never render\nat the same time. Over a game the base draws only a small \"To the lobby\" button.\n\n- A system tab is hidden while the title has nothing for it (no store offers, no heroes…): the module\n registers with `available: false` and calls `ctx.features.setAvailable(id, ok, reason)`.\n- `ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type);\n the lobby builds \"Play\" from the `game` ones. Switch with `ctx.navigate(id)` (`\"lobby\"` is the base).\n- Open another system's screen by id: `ctx.features.open(\"store\")` — from a game too (the host shows\n it in a window while the lobby is off screen). Never import another module.\n\n## Share progress across modes (the merge)\n\nShared state lives in the ONE SDK client, not in any module. Every module reads/writes the same\n`client` (currency, inventory, characters), so progress carries across modes automatically:\n\n- Gold earned idle in the RPG mode is spendable in the Board mode and in the shop — same\n `client.currency`, same cache. No cross-module plumbing needed for durable state.\n- For live cross-module signals (not durable state), use `ctx.events` with a **typed topic** — see\n \"Signals between modules\" below.\n- Modules never import each other's files or `src/base/`. The client and the event bus are the seams\n between them — which is what keeps a module reusable in another game. Code several of THIS game's\n modules need (shared types, the game's UI kit, helpers) goes to `src/shared/`, which never imports a\n module (see idosgames-project-structure).\n\n## Shared chrome: roles, and the base takes them\n\nBalances, the crypto wallet, the status line, \"Log out\" / player ID, the menu of features and the\nswitch between modes are **shared-UI roles** (`currency-bar | wallet | status | account | menu |\nmodes`). A module that draws one for EVERY mode declares it statically on the module object —\nthe base does:\n\n```ts\ndefineModule({\n id: \"base\",\n meta: { name: \"Base\", type: \"app\", engine: \"dom\" },\n sharedUi: {\n provides: [\"currency-bar\", \"wallet\", \"account\", \"menu\", \"modes\"],\n },\n setup(ctx) {\n /* the lobby: balances, wallet, profile, tabs, \"Play\" */\n },\n});\n```\n\nThe host resolves role owners from these declarations BEFORE any `setup()` — the first provider wins,\na conflict is warned — and every module asks `ctx.sharedUi.shouldDraw(role)`. A game draws its own\ncopy only while nobody took the role. So:\n\n- Next to the base a game hides its own balances and wallet by itself. Do NOT edit a catalog game to\n remove them: it does that itself, and an edit marks it customized (no more catalog updates).\n- `status` — the line of a game's own actions (\"Rolling…\") — stays with each game.\n- A game installed without a base stays complete — nobody owns a role, it draws everything, and the\n host draws its own nav of routes, ☰ and \"Log out\" (those are the host-drawn roles `modes`, `menu`,\n `account`).\n- A new module that needs, say, the balance bar but does not draw it declares\n `sharedUi: { requires: [\"currency-bar\"] }`; with no provider installed the host warns.\n\n**Layout is the host's job.** A game's overlay fills the screen under the base's small button; a\nscene's own DOM HUD can keep clear of host chrome with the CSS variables on the host root —\n`top: calc(var(--idos-safe-top, 0px) + 8px)`, `bottom: calc(var(--idos-safe-bottom, 0px) + 8px)`\n(voxelcraft's hotbar does this). Both are 0 when nothing is pinned. `hud`-slot panels are wrapped in\n`display: contents`, so a panel can stretch (`flex: 1`) and decide its own pointer-events: let clicks\nthrough to the game, take them only on controls.\n\n## Signals between modules (typed topics)\n\nA topic is a token passed by value: the name carries the major version, the payload is a flat JSON\nshape — one artifact that gives the TS type, the runtime check and the catalog entry.\n\n```ts\n// idle-rpg/events.ts — the EMITTER owns the topic\nimport { defineTopic, shape } from \"@idosgames/module-sdk\";\nexport const characterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ characterId: \"string\", level: \"number\" }),\n);\n// setup(): hand the panel a callback that does\n// ctx.events.emit(characterUpgraded, { characterId, level });\n```\n\n```ts\n// board-game/events.ts — the LISTENER keeps its OWN copy (copied from the catalog), never an import.\n// Optional: idle-rpg may not be installed. Only the fields this module reads.\nexport const idleCharacterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ level: \"number\" }),\n);\n// setup() — NOT a panel effect (activeOnly panels unmount with their mode and miss events):\nctx.events.on(idleCharacterUpgraded, ({ level }) =>\n news.push(`Idle RPG hero reached Lv ${level}`),\n);\n```\n\nDeclare both sides in `module.meta.json` — the catalog and the agent read this, not your code:\n\n```json\n\"events\": {\n \"emits\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"when\": \"a hero levelled up\",\n \"payload\": { \"characterId\": \"string\", \"level\": \"number\" } }],\n \"listens\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"why\": \"news chip in the board HUD\" }]\n}\n```\n\nRules:\n\n- Name `<your-module-id>:<event>@<major>`. Emit only into your own namespace — the host drops an\n emit into another module's namespace with `console.error`; `host:` is reserved. The only topics\n you import are `hostTopics` from `@idosgames/module-sdk` (`hostTopics.modeChanged` =\n `host:mode-changed@1 { from?: string; to: string }`, sent on every mode switch).\n- Shape leaves: `\"string\" | \"number\" | \"boolean\"`, with `\"[]\"` and/or a trailing `\"?\"`; nested\n objects allowed. Payloads are plain JSON — no functions, class instances or engine objects.\n- An incompatible change is a NEW topic `@2` (send both during the transition); within a major, only\n add fields. A listener whose fields are missing is skipped (one `console.warn`), never crashed.\n- An event is a signal, not state, and is never replayed — \"what exists now\" is SDK data or\n `ctx.sharedUi`. No request/response between modules: if you need an answer, it is data.\n- A throwing handler does not stop other listeners or reach the emitter; the host removes every\n subscription on logout, so re-login does not double handlers.\n- In the AI editor's preview, `gameState` returns the last 50 events, who listens to what, and hints\n like \"board-game listens to x@1 but only x@2 is emitted\".\n- A game's OWN modules may keep shared topic tokens in `src/shared/events/` and import them on both\n sides — still declare them in each `module.meta.json`.\n\n## Reference app\n\nThe base + the systems + `board-game` + `idle-rpg`:\n\n- sign-in → the lobby: balances, wallet and profile on top, the systems as tabs, \"Play\" with two games;\n- \"Play\" → one engine scene mounted at a time; the base's button returns to the lobby;\n- the games hide their own balances and wallet (the lobby has them); their status lines stay theirs;\n- currency changed in one mode is immediately visible in the others and in the lobby; levelling a hero\n in Idle RPG shows a news chip on the board (`idle-rpg:character-upgraded@1`).\n\nTo pull the modules, use `get_module {id}` for each (MCP) and register them as above. Adjust layouts\nper module (a full-bleed overlay UI vs a docked side panel) — see each module's RootPanel.\n",
3
+ "description": "Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards, quests, chests, multiplayer…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system, add a second game, switch between the lobby and game modes, share currency/inventory across modules, or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi, shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single module).",
4
+ "content": "---\nname: idosgames-compose-modules\ndescription: >-\n Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards,\n quests, chests, multiplayer…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this\n whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system,\n add a second game, switch between the lobby and game modes, share currency/inventory across modules,\n or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi,\n shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared\n state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single\n module).\n---\n\n# Putting an app together: the base + modules\n\nEvery project is **the base + modules**. The base (`src/base/`, from the host scaffold) is sign-in and\nthe lobby; everything else is a module in `src/modules/<id>/`:\n\n- **systems** (`type: feature`) — one per player-facing module of the engine: store, premium (VIP),\n character, inventory, lootboxes, boosts, leaderboard, rewards (daily), quests, collections, craft,\n marketplace, deals, events, coop-events, season, friends, chat, invite, pvp, multiplayer, profile,\n tutorials, workshop, ai, mailbox. Each registers its screen as a **feature** → a tab of the lobby.\n The base installs only six of them into every new project (`inBase: true`: profile, inventory,\n invite, friends, quests, mailbox); every other system is installed only when the app needs it — a game\n brings the ones it lists in `uses`, and you install any other one as soon as the task needs what\n it `provides` (InstallModule / `get_module`) — the user does not have to name the module; reuse it\n instead of writing that feature again. Never install one the task does not need.\n- **games** (`type: template`) — board-game, idle-rpg, voxelcraft, mini-game, or the game's own. Each\n registers a **route** → a \"Play\" button in the lobby. A game that plugs into a system lists it in\n `uses` in its module.meta.json (`ctx.multiplayer` → `\"multiplayer\"`, `ctx.content` → `\"workshop\"`;\n `npm run check` enforces those two) — install those together with the game.\n\n## Register modules\n\n```ts\n// src/modules.ts — the platform regenerates it; by hand it is imports + the array\nexport const modules: Module[] = [\n storeModule, // systems: lobby tabs\n characterModule,\n leaderboardModule,\n boardGameModule, // games: \"Play\"\n idleRpgModule,\n];\n```\n\n`src/main.tsx` mounts `[baseModule, ...modules]`. After sign-in the player lands in the lobby\n(`src/app.config.ts` `start` can send them straight into a game). \"Play\" switches the host to the\ngame's mode: only the active mode's scene is mounted and ticking; the rest are suspended (their RAF\nstops). This is why two different engines (Three + Phaser) live in one project — they never render\nat the same time. Over a game the base draws only a small \"To the lobby\" button.\n\n- A system tab is hidden while the title has nothing for it (no store offers, no heroes…): the module\n registers with `available: false` and calls `ctx.features.setAvailable(id, ok, reason)`.\n- `ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type);\n the lobby builds \"Play\" from the `game` ones. Switch with `ctx.navigate(id)` (`\"lobby\"` is the base).\n- Open another system's screen by id: `ctx.features.open(\"store\")` — from a game too (the host shows\n it in a window while the lobby is off screen). Never import another module.\n\n## Share progress across modes (the merge)\n\nShared state lives in the ONE SDK client, not in any module. Every module reads/writes the same\n`client` (currency, inventory, characters), so progress carries across modes automatically:\n\n- Gold earned idle in the RPG mode is spendable in the Board mode and in the shop — same\n `client.currency`, same cache. No cross-module plumbing needed for durable state.\n- For live cross-module signals (not durable state), use `ctx.events` with a **typed topic** — see\n \"Signals between modules\" below.\n- Modules never import each other's files or `src/base/`. The client and the event bus are the seams\n between them — which is what keeps a module reusable in another game. Code several of THIS game's\n modules need (shared types, the game's UI kit, helpers) goes to `src/shared/`, which never imports a\n module (see idosgames-project-structure).\n\n## Shared chrome: roles, and the base takes them\n\nBalances, the crypto wallet, the status line, \"Log out\" / player ID, the menu of features and the\nswitch between modes are **shared-UI roles** (`currency-bar | wallet | status | account | menu |\nmodes`). A module that draws one for EVERY mode declares it statically on the module object —\nthe base does:\n\n```ts\ndefineModule({\n id: \"base\",\n meta: { name: \"Base\", type: \"app\", engine: \"dom\" },\n sharedUi: {\n provides: [\"currency-bar\", \"wallet\", \"account\", \"menu\", \"modes\"],\n },\n setup(ctx) {\n /* the lobby: balances, wallet, profile, tabs, \"Play\" */\n },\n});\n```\n\nThe host resolves role owners from these declarations BEFORE any `setup()` — the first provider wins,\na conflict is warned — and every module asks `ctx.sharedUi.shouldDraw(role)`. A game draws its own\ncopy only while nobody took the role. So:\n\n- Next to the base a game hides its own balances and wallet by itself. Do NOT edit a catalog game to\n remove them: it does that itself, and an edit marks it customized (no more catalog updates).\n- `status` — the line of a game's own actions (\"Rolling…\") — stays with each game.\n- A game installed without a base stays complete — nobody owns a role, it draws everything, and the\n host draws its own nav of routes, ☰ and \"Log out\" (those are the host-drawn roles `modes`, `menu`,\n `account`).\n- A new module that needs, say, the balance bar but does not draw it declares\n `sharedUi: { requires: [\"currency-bar\"] }`; with no provider installed the host warns.\n\n**Layout is the host's job.** A game's overlay fills the screen under the base's small button; a\nscene's own DOM HUD can keep clear of host chrome with the CSS variables on the host root —\n`top: calc(var(--idos-safe-top, 0px) + 8px)`, `bottom: calc(var(--idos-safe-bottom, 0px) + 8px)`\n(voxelcraft's hotbar does this). Both are 0 when nothing is pinned. `hud`-slot panels are wrapped in\n`display: contents`, so a panel can stretch (`flex: 1`) and decide its own pointer-events: let clicks\nthrough to the game, take them only on controls.\n\n## Signals between modules (typed topics)\n\nA topic is a token passed by value: the name carries the major version, the payload is a flat JSON\nshape — one artifact that gives the TS type, the runtime check and the catalog entry.\n\n```ts\n// idle-rpg/events.ts — the EMITTER owns the topic\nimport { defineTopic, shape } from \"@idosgames/module-sdk\";\nexport const characterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ characterId: \"string\", level: \"number\" }),\n);\n// setup(): hand the panel a callback that does\n// ctx.events.emit(characterUpgraded, { characterId, level });\n```\n\n```ts\n// board-game/events.ts — the LISTENER keeps its OWN copy (copied from the catalog), never an import.\n// Optional: idle-rpg may not be installed. Only the fields this module reads.\nexport const idleCharacterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ level: \"number\" }),\n);\n// setup() — NOT a panel effect (activeOnly panels unmount with their mode and miss events):\nctx.events.on(idleCharacterUpgraded, ({ level }) =>\n news.push(`Idle RPG hero reached Lv ${level}`),\n);\n```\n\nDeclare both sides in `module.meta.json` — the catalog and the agent read this, not your code:\n\n```json\n\"events\": {\n \"emits\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"when\": \"a hero levelled up\",\n \"payload\": { \"characterId\": \"string\", \"level\": \"number\" } }],\n \"listens\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"why\": \"news chip in the board HUD\" }]\n}\n```\n\nRules:\n\n- Name `<your-module-id>:<event>@<major>`. Emit only into your own namespace — the host drops an\n emit into another module's namespace with `console.error`; `host:` is reserved. The only topics\n you import are `hostTopics` from `@idosgames/module-sdk` (`hostTopics.modeChanged` =\n `host:mode-changed@1 { from?: string; to: string }`, sent on every mode switch).\n- Shape leaves: `\"string\" | \"number\" | \"boolean\"`, with `\"[]\"` and/or a trailing `\"?\"`; nested\n objects allowed. Payloads are plain JSON — no functions, class instances or engine objects.\n- An incompatible change is a NEW topic `@2` (send both during the transition); within a major, only\n add fields. A listener whose fields are missing is skipped (one `console.warn`), never crashed.\n- An event is a signal, not state, and is never replayed — \"what exists now\" is SDK data or\n `ctx.sharedUi`. No request/response between modules: if you need an answer, it is data.\n- A throwing handler does not stop other listeners or reach the emitter; the host removes every\n subscription on logout, so re-login does not double handlers.\n- In the AI editor's preview, `gameState` returns the last 50 events, who listens to what, and hints\n like \"board-game listens to x@1 but only x@2 is emitted\".\n- A game's OWN modules may keep shared topic tokens in `src/shared/events/` and import them on both\n sides — still declare them in each `module.meta.json`.\n\n## Reference app\n\nThe base + the systems + `board-game` + `idle-rpg`:\n\n- sign-in → the lobby: balances, wallet and profile on top, the systems as tabs, \"Play\" with two games;\n- \"Play\" → one engine scene mounted at a time; the base's button returns to the lobby;\n- the games hide their own balances and wallet (the lobby has them); their status lines stay theirs;\n- currency changed in one mode is immediately visible in the others and in the lobby; levelling a hero\n in Idle RPG shows a news chip on the board (`idle-rpg:character-upgraded@1`).\n\nTo pull the modules, use `get_module {id}` for each (MCP) and register them as above. Adjust layouts\nper module (a full-bleed overlay UI vs a docked side panel) — see each module's RootPanel.\n",
5
5
  "references": []
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idosgames-getting-started",
3
3
  "description": "Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as the project grows). If pulling modules over MCP, use the @idosgames/mcp tools get_host_scaffold / get_module / get_manifest.",
4
- "content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in\n + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and\n the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever\n a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a\n project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or\n src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and\n idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as\n the project grows). If pulling modules over MCP, use the @idosgames/mcp\n tools get_host_scaffold / get_module / get_manifest.\n---\n\n# Getting started (iDosGames composable modules)\n\nAn iDosGames project is **one host shell + N feature modules**. The host owns everything that exists\nonce — the SDK client, login, the React root, the screen, a module registry, and a Mode Router that\nswitches between modules. A module is a library (a game or app) that plugs into the host; it never\ncreates the client, logs in, or owns the root.\n\n## Runtime packages (npm)\n\n- `@idosgames/core` — the SDK client (`createIDosGamesClient`): auth, currency, store, characters,\n blockchain, ~28 services. See the per-service skills (currency-system, store-system, …).\n- `@idosgames/module-sdk` — the module contract types (`Module`, `ModuleContext`, `EngineScene`,\n `UiPanel`). See idosgames-module-contract.\n- `@idosgames/react` — shared React glue (`IDosGamesProvider`, `useIDosGamesClient`, `useUserState`,\n `StatusProvider`, `createControllerContext`).\n- `@idosgames/app-shell` — the host runtime (`mountHost`, the Mode Router, the module registry).\n- `@idosgames/wallet` — optional wallet bridge (EVM/Solana) for on-chain deposits/withdrawals.\n\nInstall the exact versions from `get_manifest` (MCP) or the registry `index.json` `runtimePackages`.\n\n## Project shape\n\n```\nIDOS.md # the project guide every agent reads first (short, evergreen)\nAGENTS.md, CLAUDE.md # pointers to IDOS.md for external tools\ndocs/feature-history/ # one file per game system + README.md index (the project's memory)\nindex.html # mounts #app\nsrc/main.tsx # creates ONE client and calls mountHost({container, client, modules})\nsrc/modules.ts # the registry: export const modules: Module[] = [ … ] — imports + array only\nsrc/modules/{id}/ # each module's source + its module.meta.json (from get_module)\n```\n\nPlatform-created projects also carry `idos.modules.lock.json` (which modules came from the catalog,\nwith file fingerprints) — it is platform-owned; don't edit it. Where new code goes as the game grows,\nand how the project documents itself, is **idosgames-project-structure**.\n\n`src/main.tsx` (host-owned) is the only place that creates the client — but it does **not** sign in.\n`mountHost` owns sign-in: it replays a previous session (`autoLogin`) and renders the login screen\nwhen there is nothing to replay. Calling a `login*` method here skips that screen for good, taking\n\"switch account\" and wallet sign-in with it:\n\n```ts\nconst client = createIDosGamesClient({ titleID, buildKey, throttleMs: 0 });\n// Do NOT log in here — the host does it.\nmountHost({\n container: app,\n client,\n modules, // from ./modules\n renderLogin, // optional: your own screen; omitted = a plain guest-only default\n});\n```\n\nWhether a returning player is signed back in silently is the login screen's business, not the\nhost's: it calls `client.auth.setRememberSession(remember)` before a `login*` method. See the\nauthentication skill.\n\n`src/modules.ts` registers what the project composes:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nexport const modules: Module[] = [boardGameModule];\n```\n\n## Steps to scaffold\n\n1. **Base** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/base`): sign-in and the lobby in `src/base/`, `src/app.config.ts` (start in the lobby or\n a game), an empty `src/modules.ts` → an empty lobby.\n2. **Bind the Title** — open `src/idos.title.ts` and set `IDOS_TITLE_ID` to the game's canonical\n Title id (and `IDOS_BUILD_KEY` if the title enforces one). This file is the project's single\n centralized identity — it is the highest-priority source and the only channel a packaged mobile\n build, iframe embed, or shared link has. On platform-created projects the platform generates it;\n on a manually scaffolded project **you fill it yourself**. Do NOT bind the title via `.env.local`\n (`VITE_IDOS_TITLE_ID`) — that is a local-dev fallback for the raw template only, and it does not\n travel with the code.\n3. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`. The platform's standard set is every module with `inBase: true` (the\n systems: shop, heroes, inventory, leaderboards, rewards…) plus the game(s).\n4. **Register** — for each module add `import { {camelCase(id)}Module } from \"./modules/{id}\"` and\n push it into the `modules` array (e.g. `board-game` → `boardGameModule`). Then add the module to\n the \"Installed modules\" block of `IDOS.md` (one line: ``- `board-game` (0.1.0) — src/modules/board-game/``)\n — on platform projects the platform maintains that block itself.\n5. **Install deps** — the union of `runtimePackages` + each module's `dependencies`. Modules bring\n their own engine (three, phaser); the host brings react/react-dom/app-shell.\n6. **Run** — sign-in → the lobby: the systems' screens are its tabs (a tab hides itself while the\n title has nothing for it), every game gets a \"Play\" button, and a small button over a game returns\n to the lobby. The lobby draws the balances and the wallet; games hide their own copies by\n themselves (`sharedUi` roles).\n\nTo combine multiple genres into one game, see **idosgames-compose-modules**. To write or edit a\nmodule, see **idosgames-module-contract**.\n\n## Where state lives (decide this before writing the first save)\n\nThe project is client-side code in the player's browser. `localStorage`, module fields, and React\nstate are **not storage** — nothing there survives a device change, and nothing there is trusted.\n\n1. **A dedicated module owns it?** Use that module. Currencies, inventory, quests, characters,\n leaderboards, store purchases each have a service that enforces the rules server-side.\n2. **Otherwise, per-player data → `client.userCustomData`** — buckets `Private`/`Public` are\n client-writable (settings, cosmetics), `ReadOnly`/`Internal` are server-only. Anything a player\n could cheat by editing goes in the server-only buckets. See **user-custom-data**.\n3. **Shared by all players → `client.titleCustomData`** (event state, global counters, server\n thresholds, feature toggles). Read-only for clients. See **title-custom-data**.\n4. **Writing any of the server-only data, or any rule the player must not be able to fake** →\n a CloudCode handler, called with `client.cloudCode.execute(...)`. See **cloud-code**.\n\n## Two MCP surfaces: game CODE vs. a Title's live DATA\n\nThe platform exposes **two independent MCP servers** — don't confuse them:\n\n- **`@idosgames/mcp`** (this one) serves the **CODE registry**. Use it to WRITE a game's source:\n `get_host_scaffold`, `list_modules` / `search` / `get_module`, `get_manifest`, `list_skills` /\n `get_skill`. Transport: stdio (`npx -y @idosgames/mcp`). Its registry is bundled offline; to use the\n hosted copy set `IDOSGAMES_REGISTRY_URL=https://cloud.idosgames.com/drive/registry/latest` — a **base**\n the loader appends `/index.json`, `/modules/{id}.json`, etc. to (the base itself is not fetchable on\n R2; open `.../latest/index.json` to browse the catalog). Read-only, no auth. It never reads or changes\n a live Title's data.\n- **The Title-configuration MCP** (a separate backend server) owns a **live Title's DATA**. Use it to\n read/write the Title's `TitlePublicConfiguration` (`get_<field>` / `save_<field>`, or the whole\n model) and to generate assets (`generate_image` / `generate_audio` / `generate_text` /\n `generate_three_d` / `generate_video`). Transport: HTTP JSON-RPC at\n `POST https://site.idosgames.com/api/v2/mcp`; every tool call takes a `title_id` argument.\n Authorization is **OAuth 2.1** — there is no API key and nothing to paste. Connect it as a plain\n HTTP MCP server with **no headers**: your client gets a `401`, discovers the authorization\n server, registers itself, and opens a browser where the publisher picks which Titles and which\n permissions to grant. The token lives in your client's own credential store, so committed config\n holds only the URL:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\"\n }\n }\n }\n ```\n\n Permissions the publisher can grant: `config:read`, `config:write`, `cloudcode:write`,\n `ai:generate`. A grant is scoped to the Titles ticked on the consent screen, and the publisher\n can revoke it any time from **Connected apps** in the dashboard. If a call comes back\n `SCOPE_NOT_ALLOWED` or `TITLE_NOT_ALLOWED`, the token is fine — that permission or that Title\n simply was not granted; ask the publisher to re-authorize rather than retrying.\n\n Configuring a **fresh (empty) Title** so a game can actually run against it (currencies → game\n loop → bots) is its own checklist: see **idosgames-title-bootstrap**.\n\nRule of thumb: **game CODE → `@idosgames/mcp`; a Title's live config DATA and generated ASSETS → the\nbackend `v2/mcp` server.** Scaffolding a project and configuring/populating the Title it runs as are\ntwo different jobs on two different servers — connect the one that matches the task (or both).\n",
4
+ "content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in\n + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and\n the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever\n a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a\n project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or\n src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and\n idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as\n the project grows). If pulling modules over MCP, use the @idosgames/mcp\n tools get_host_scaffold / get_module / get_manifest.\n---\n\n# Getting started (iDosGames composable modules)\n\nAn iDosGames project is **one host shell + N feature modules**. The host owns everything that exists\nonce — the SDK client, login, the React root, the screen, a module registry, and a Mode Router that\nswitches between modules. A module is a library (a game or app) that plugs into the host; it never\ncreates the client, logs in, or owns the root.\n\n## Runtime packages (npm)\n\n- `@idosgames/core` — the SDK client (`createIDosGamesClient`): auth, currency, store, characters,\n blockchain, ~28 services. See the per-service skills (currency-system, store-system, …).\n- `@idosgames/module-sdk` — the module contract types (`Module`, `ModuleContext`, `EngineScene`,\n `UiPanel`). See idosgames-module-contract.\n- `@idosgames/react` — shared React glue (`IDosGamesProvider`, `useIDosGamesClient`, `useUserState`,\n `StatusProvider`, `createControllerContext`).\n- `@idosgames/app-shell` — the host runtime (`mountHost`, the Mode Router, the module registry).\n- `@idosgames/wallet` — optional wallet bridge (EVM/Solana) for on-chain deposits/withdrawals.\n\nInstall the exact versions from `get_manifest` (MCP) or the registry `index.json` `runtimePackages`.\n\n## Project shape\n\n```\nIDOS.md # the project guide every agent reads first (short, evergreen)\nAGENTS.md, CLAUDE.md # pointers to IDOS.md for external tools\ndocs/feature-history/ # one file per game system + README.md index (the project's memory)\nindex.html # mounts #app\nsrc/main.tsx # creates ONE client and calls mountHost({container, client, modules})\nsrc/modules.ts # the registry: export const modules: Module[] = [ … ] — imports + array only\nsrc/modules/{id}/ # each module's source + its module.meta.json (from get_module)\n```\n\nPlatform-created projects also carry `idos.modules.lock.json` (which modules came from the catalog,\nwith file fingerprints) — it is platform-owned; don't edit it. Where new code goes as the game grows,\nand how the project documents itself, is **idosgames-project-structure**.\n\n`src/main.tsx` (host-owned) is the only place that creates the client — but it does **not** sign in.\n`mountHost` owns sign-in: it replays a previous session (`autoLogin`) and renders the login screen\nwhen there is nothing to replay. Calling a `login*` method here skips that screen for good, taking\n\"switch account\" and wallet sign-in with it:\n\n```ts\nconst client = createIDosGamesClient({ titleID, buildKey, throttleMs: 0 });\n// Do NOT log in here — the host does it.\nmountHost({\n container: app,\n client,\n modules, // from ./modules\n renderLogin, // optional: your own screen; omitted = a plain guest-only default\n});\n```\n\nWhether a returning player is signed back in silently is the login screen's business, not the\nhost's: it calls `client.auth.setRememberSession(remember)` before a `login*` method. See the\nauthentication skill.\n\n`src/modules.ts` registers what the project composes:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nexport const modules: Module[] = [boardGameModule];\n```\n\n## Steps to scaffold\n\n1. **Base** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/base`): sign-in and the lobby in `src/base/`, `src/app.config.ts` (start in the lobby or\n a game), an empty `src/modules.ts` → an empty lobby.\n2. **Bind the Title** — open `src/idos.title.ts` and set `IDOS_TITLE_ID` to the game's canonical\n Title id (and `IDOS_BUILD_KEY` if the title enforces one). This file is the project's single\n centralized identity — it is the highest-priority source and the only channel a packaged mobile\n build, iframe embed, or shared link has. On platform-created projects the platform generates it;\n on a manually scaffolded project **you fill it yourself**. Do NOT bind the title via `.env.local`\n (`VITE_IDOS_TITLE_ID`) — that is a local-dev fallback for the raw template only, and it does not\n travel with the code.\n3. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`. The base's systems are the modules with `inBase: true` (profile,\n inventory, invite, friends, quests, mailbox); add the game(s) with the modules each game `uses`, and any\n other system (shop, VIP, leaderboards…) only when the app needs it.\n4. **Register** — for each module add `import { {camelCase(id)}Module } from \"./modules/{id}\"` and\n push it into the `modules` array (e.g. `board-game` → `boardGameModule`). Then add the module to\n the \"Installed modules\" block of `IDOS.md` (one line: ``- `board-game` (0.1.0) — src/modules/board-game/``)\n — on platform projects the platform maintains that block itself.\n5. **Install deps** — the union of `runtimePackages` + each module's `dependencies`. Modules bring\n their own engine (three, phaser); the host brings react/react-dom/app-shell.\n6. **Run** — sign-in → the lobby: the systems' screens are its tabs (a tab hides itself while the\n title has nothing for it), every game gets a \"Play\" button, and a small button over a game returns\n to the lobby. The lobby draws the balances and the wallet; games hide their own copies by\n themselves (`sharedUi` roles).\n\nTo combine multiple genres into one game, see **idosgames-compose-modules**. To write or edit a\nmodule, see **idosgames-module-contract**.\n\n## Where state lives (decide this before writing the first save)\n\nThe project is client-side code in the player's browser. `localStorage`, module fields, and React\nstate are **not storage** — nothing there survives a device change, and nothing there is trusted.\n\n1. **A dedicated module owns it?** Use that module. Currencies, inventory, quests, characters,\n leaderboards, store purchases each have a service that enforces the rules server-side.\n2. **Otherwise, per-player data → `client.userCustomData`** — buckets `Private`/`Public` are\n client-writable (settings, cosmetics), `ReadOnly`/`Internal` are server-only. Anything a player\n could cheat by editing goes in the server-only buckets. See **user-custom-data**.\n3. **Shared by all players → `client.titleCustomData`** (event state, global counters, server\n thresholds, feature toggles). Read-only for clients. See **title-custom-data**.\n4. **Writing any of the server-only data, or any rule the player must not be able to fake** →\n a CloudCode handler, called with `client.cloudCode.execute(...)`. See **cloud-code**.\n\n## Two MCP surfaces: game CODE vs. a Title's live DATA\n\nThe platform exposes **two independent MCP servers** — don't confuse them:\n\n- **`@idosgames/mcp`** (this one) serves the **CODE registry**. Use it to WRITE a game's source:\n `get_host_scaffold`, `list_modules` / `search` / `get_module`, `get_manifest`, `list_skills` /\n `get_skill`. Transport: stdio (`npx -y @idosgames/mcp`). Its registry is bundled offline; to use the\n hosted copy set `IDOSGAMES_REGISTRY_URL=https://cloud.idosgames.com/drive/registry/latest` — a **base**\n the loader appends `/index.json`, `/modules/{id}.json`, etc. to (the base itself is not fetchable on\n R2; open `.../latest/index.json` to browse the catalog). Read-only, no auth. It never reads or changes\n a live Title's data.\n- **The Title-configuration MCP** (a separate backend server) owns a **live Title's DATA**. Use it to\n read/write the Title's `TitlePublicConfiguration` (`get_<field>` / `save_<field>`, or the whole\n model) and to generate assets (`generate_image` / `generate_audio` / `generate_text` /\n `generate_three_d` / `generate_video`). Transport: HTTP JSON-RPC at\n `POST https://site.idosgames.com/api/v2/mcp`; every tool call takes a `title_id` argument.\n Authorization is **OAuth 2.1** — there is no API key and nothing to paste. Connect it as a plain\n HTTP MCP server with **no headers**: your client gets a `401`, discovers the authorization\n server, registers itself, and opens a browser where the publisher picks which Titles and which\n permissions to grant. The token lives in your client's own credential store, so committed config\n holds only the URL:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\"\n }\n }\n }\n ```\n\n Permissions the publisher can grant: `config:read`, `config:write`, `cloudcode:write`,\n `ai:generate`. A grant is scoped to the Titles ticked on the consent screen, and the publisher\n can revoke it any time from **Connected apps** in the dashboard. If a call comes back\n `SCOPE_NOT_ALLOWED` or `TITLE_NOT_ALLOWED`, the token is fine — that permission or that Title\n simply was not granted; ask the publisher to re-authorize rather than retrying.\n\n Configuring a **fresh (empty) Title** so a game can actually run against it (currencies → game\n loop → bots) is its own checklist: see **idosgames-title-bootstrap**.\n\nRule of thumb: **game CODE → `@idosgames/mcp`; a Title's live config DATA and generated ASSETS → the\nbackend `v2/mcp` server.** Scaffolding a project and configuring/populating the Title it runs as are\ntwo different jobs on two different servers — connect the one that matches the task (or both).\n",
5
5
  "references": []
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idosgames-module-contract",
3
- "description": "Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk: the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route registration, and the shared controller-box bridge between an imperative scene and React panels. Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg, voxelcraft, mini-game, store, character, workshop…), or asks about defineModule, registerScene/registerPanel/registerRoute, ctx.features (register / open / setBadge / setAvailable), ctx.modes, activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic, ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content), ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.",
4
- "content": "---\nname: idosgames-module-contract\ndescription: >-\n Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk:\n the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route\n registration, and the shared controller-box bridge between an imperative scene and React panels.\n Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg,\n voxelcraft, mini-game, store, character, workshop…), or asks about defineModule,\n registerScene/registerPanel/registerRoute, ctx.features (register / open / setBadge / setAvailable),\n ctx.modes,\n activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic,\n ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content),\n ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.\n---\n\n# The module contract (@idosgames/module-sdk)\n\nA module is a manifest the host installs once. It exports `{camelCase(id)}Module` from its\n`index.ts` (e.g. `board-game` → `boardGameModule`, `idle-rpg` → `idleRpgModule`) — the host seeder\nderives the import name from the id, so this convention is required.\n\n```ts\nimport { defineModule } from \"@idosgames/module-sdk\";\n\nexport const boardGameModule = defineModule({\n id: \"board-game\",\n meta: { name: \"Board Game\", type: \"game\", genre: \"board\", engine: \"three\" }, // engine: three|phaser|dom\n setup(ctx) {\n // ctx.client (shared, authed) · ctx.events (typed cross-module bus) · ctx.sharedUi · ctx.surface\n ctx.registerScene(createScene(box)); // rendered engine scene (optional)\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: RootPanel }); // React UI (optional)\n ctx.registerRoute({ id: \"board-game\", label: \"Board\", icon: \"🎲\" }); // a mode → the lobby's \"Play\"\n },\n});\n```\n\nTwo kinds of module plug into the base (`src/base/` — sign-in and the lobby):\n\n- a **game** (`meta.type: \"game\"`) registers a route: the lobby shows a \"Play\" button for it, and\n over it the base draws only a small \"To the lobby\" button;\n- a **system** (`meta.type: \"app\"`) registers a feature — its screen becomes a lobby tab:\n `ctx.features.register({ id, label, icon, group, order, primary?, available?, Screen })`. A tab\n that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows (details: **idosgames-ui-kit**).\n\n`ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type),\nfilled once all modules are installed (read it at render time, not inside `setup()`).\n`ctx.navigate(id)` switches (`\"lobby\"` is the base); called inside `setup()` it picks the mode the app\nstarts in.\n\nOptional static field: `sharedUi: { provides?: SharedUiRole[]; requires?: SharedUiRole[] }` — see\n\"Shared chrome\" below.\n\n`meta.type` is `game | app | ai-app`; `meta.engine` is `three | phaser | dom` (`dom` = no renderer —\na pure React/DOM app module). `setup` is called ONCE with `ctx: ModuleContext`.\n\n## EngineScene (the rendered part)\n\nA scene mounts into a bare `HTMLElement` (framework-free — this is why a vanilla Three game like\nvoxelcraft fits). The host's Mode Router drives it; only the active mode runs.\n\n```ts\nconst scene: EngineScene = {\n surface: \"fullbleed-canvas\",\n mount(ctx: SceneMountContext) {\n controller = new Controller(ctx.host);\n box.set(controller);\n },\n activate() {\n controller?.setRunning(true);\n }, // became the active mode → resume RAF\n suspend() {\n controller?.setRunning(false);\n }, // hidden → stop RAF (invariant: only active ticks)\n destroy() {\n controller?.destroy();\n }, // permanent teardown\n};\n```\n\n`mount` takes a context object (not a bare element) so it can grow — e.g. a host-shared renderer in\nthe composition era — without breaking the contract. A scene MUST stop its RAF on `suspend`.\n\nA module that registers a scene MUST also publish its debug surface — `ctx.exposeToAgent({ state,\nactions, describeActions })` — and must NOT gate controls on Pointer Lock (unavailable in the\npreview's cross-origin iframe). Nothing inside a `<canvas>` is observable from the DOM, so without\nthe surface neither the AI Coder nor a human reviewer can tell what the game is doing. See the\n`idosgames-agent-debug-surface` skill.\n\n### `capture()` — the frame for \"Take a shot and share\"\n\nidosgames.com has a **Shot** button under the game: it asks the game for one frame, puts the\nplatform's branding and a QR code on it and offers to post it. A scene answers through the optional\n`capture?(): Promise<Blob | null>`. The host (`@idosgames/app-shell`) wires the request itself —\na module only has to return a picture of its active scene.\n\n**Implement it in any scene that renders to a canvas.** Without it the host falls back to reading\nthe `<canvas>` from outside, and for WebGL that is a trap: a renderer created WITHOUT\n`preserveDrawingBuffer` (the default, and the right default — the flag costs memory and a copy every\nframe) has already cleared its buffer by the time anyone reads it, so `toBlob()` returns a **fully\nblack** image and reports nothing. The host rejects single-colour frames, so the player gets the\ngame's cover instead of their moment.\n\nRender and read in the SAME tick:\n\n```ts\n// three.js\ncapture() {\n return new Promise((resolve) => {\n renderer.render(scene, camera); // draw now…\n renderer.domElement.toBlob(resolve, \"image/png\"); // …and read before the buffer is cleared\n });\n},\n\n// Phaser — use its own snapshot, it handles the timing\ncapture() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(null), 1500); // no frame came (context lost) → answer \"no\", don't hang\n game.loop.wake(); // a sleeping loop never draws again → snapshot never fires\n game.renderer.snapshot((image) => {\n if (!(image instanceof HTMLImageElement)) return resolve(null);\n const c = document.createElement(\"canvas\");\n c.width = image.naturalWidth || image.width;\n c.height = image.naturalHeight || image.height;\n c.getContext(\"2d\")?.drawImage(image, 0, 0);\n c.toBlob(resolve, \"image/png\");\n });\n });\n},\n```\n\nAlways answer — with a Blob or `null`. A `capture()` that never settles makes the site wait out its\ntimeout before it falls back, and the player stares at a spinner.\n\nReturn `null` when there is nothing worth showing (loading, no scene yet) — the site then uses the\ncover. Only the scene is captured, not the React panels over it: a shot for a feed reads better\nwithout the HUD. DOM-only modules (no canvas) simply omit `capture`. Working references:\n`modules/board-game` and `modules/voxelcraft` (three), `modules/idle-rpg` (Phaser).\n\n## UiPanel (the React part)\n\nPanels are React components the host renders inside its provider stack (client + status already\nprovided). Use `@idosgames/react` hooks (`useIDosGamesClient`, `useUserState`) — do NOT re-create\nproviders.\n\n- `slot`: `hud | sidebar | overlay | modal`.\n- `activeOnly` (default true): show only while this module's mode is active. Set `false` for shared\n chrome that stays across every mode (e.g. a persistent HUD).\n- `overlay`/`sidebar` layers sit between the shared HUD and the nav (the host measures both), so a\n panel positioned `absolute` inside its layer never ends up under them. Things drawn outside the\n layers use `var(--idos-safe-top, 0px)` / `var(--idos-safe-bottom, 0px)`.\n- A `hud` panel's wrapper is `display: contents`: the panel is a direct flex child of the HUD row\n (can `flex: 1`) and must set `pointer-events: auto` only on its controls.\n\n## Shared chrome (`sharedUi`)\n\nRoles: `currency-bar` (virtual-currency balances), `wallet` (crypto wallet entry — `LazyWalletPanel`),\n`status` (the `useStatus()` line), `account` (Log out + player ID), `menu` (the menu of features),\n`modes` (the switch between modes). The base takes `currency-bar`, `wallet`, `account`, `menu` and\n`modes` — its lobby draws them; `status` stays with each game. A module that draws one for every\nmode declares `sharedUi: { provides: [...] }` on the module object — statically, so the host resolves\nowners before any `setup()` and the answer never depends on module order.\n\n**Rule for templates: yield every role you draw yourself.** Read it once in `setup()` and close over\nthe answer (it is fixed for the session):\n\n```ts\nsetup(ctx) {\n const chrome = {\n wallet: ctx.sharedUi.shouldDraw(\"wallet\"),\n balances: ctx.sharedUi.shouldDraw(\"currency-bar\"),\n status: ctx.sharedUi.shouldDraw(\"status\"),\n };\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: makeRootPanel(box, chrome) });\n}\n```\n\nHide only the SHARED pieces — genre UI (the board's stage/tile/cycle chips) always stays. Never\nwrap your UI in your own `<StatusProvider>`: the host provides ONE, and a nested one would swallow\nyour messages so the shared status line never sees them. A module that needs a role but does not\ndraw it declares `sharedUi: { requires: [\"currency-bar\"] }`. `ctx.sharedUi.ownerOf(role)` names the\nprovider (or `null`). A full-screen mode (its own bottom tab bar) that shows the player ID and\n\"Log out\" in its own header declares `sharedUi: { drawsOwn: [\"account\"] }` and draws them only while\n`shouldDraw(\"account\")`: the host then hides its floating bottom-left copy while that mode is on\nscreen (it would cover the tab bar) and keeps it in the other modes. `drawsOwn` takes no role —\nwith the base installed the lobby draws the account and the mode hides its copy.\n\n## Events (`ctx.events`)\n\nTyped topic tokens — `defineTopic(\"<your-id>:<event>@1\", shape({ … }))` from\n`@idosgames/module-sdk`; emit only your own namespace; to listen to another module, copy its topic\nand payload descriptor from the catalog into your own `defineTopic` (never import it); declare both\nin `module.meta.json` (`events.emits` / `events.listens`). **Subscribe in `setup()`**, not in a panel\neffect — `activeOnly` panels unmount with their mode and miss events; store what arrives in a small\nstore the panel reads. Full rules and examples: **idosgames-compose-modules** (\"Signals between\nmodules\"). The string overloads (`emit(\"x\", …)`) are deprecated.\n\n## Content types (`ctx.content`) and switching modes (`ctx.navigate`)\n\nA game whose players make things (worlds, levels, skins) registers a **content type handler** so the\nshared Workshop module can publish and open them — the Workshop never imports the game:\n\n```ts\nsetup(ctx) {\n ctx.content.registerType({\n type: \"voxelcraft.world\", // = a key of Workshop.ContentTypes in the title config\n label: \"VoxelCraft world\",\n icon: \"⛏️\",\n modeId: \"voxelcraft\", // route to switch to after open()\n async listLocal() { return saves.map((s) => ({ id: s.id, name: s.name, updatedAt: s.at })); },\n async capture(localId) { // roles/MIME types must fit the title's content type config\n return { files: [{ role: \"main\", contentType: \"application/json\", data: json }],\n thumbnail: { contentType: \"image/webp\", data: webp }, suggestedTitle: name };\n },\n async open(content) { importSave(content.files[0].data); },\n });\n}\n```\n\nHandlers registered in `setup()` are removed with the module; `registerType` returns an unregister\nfunction for anything dynamic. `ctx.navigate(modeId)` switches the host to another module's route\n(the Workshop uses it to jump into the game after `open`); an unknown id is ignored. Details:\n**workshop-system**.\n\n## Bridging scene ↔ panel\n\nThe scene creates its controller at `mount` (needs the canvas), but panels render before that. Share\nit with a one-slot observable and read it with `useSyncExternalStore`:\n\n```ts\nexport function createControllerBox<T>() {\n /* get/set/subscribe */\n}\n// panel: const controller = useSyncExternalStore(box.subscribe, box.get); if (!controller) return <Loading/>;\n```\n\nFor the module's controller React context use the shared factory instead of hand-writing it:\n\n```ts\nexport const [BoardControllerProvider, useBoardController] =\n createControllerContext<BoardController>(\"BoardController\"); // from @idosgames/react\n```\n\n## Layout and boundaries\n\nA module is one folder, `src/modules/<id>/`. It imports only its own files, the game's shared code\nin `src/shared/` and npm packages — never another module's files or host files. Talk to other\nmodules through the shared `ctx.client` (durable state) and `ctx.events` (live signals, typed\ntopics `<module-id>:<event>@<major>` declared in `module.meta.json`). A module shipped in the catalog is fully self-contained — it never uses\n`src/shared/`, so it installs into any game; a game's own module may use it, and its shared code is\ncopied into it when it is published for other creators.\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ … })\n module.meta.json manifest: type (template|feature), summary, provides, tags, version, author\n components/ game/ data/ react/ — as needed\n```\n\nEvery module carries its own `module.meta.json` (the `ModuleManifest` shape), including a game's own\nmodules. The full standard — where a new feature goes, file size, documentation — is\n**idosgames-project-structure**.\n\n## Dependencies\n\nDeclare only the module's UNIQUE deps (e.g. `phaser` for idle-rpg, `three` for board/voxel) plus the\nshared baseline (react, @idosgames/*). The platform pins shared libs identically across modules; two\nmodules must not request different versions of one package (the build allowlist rejects it).\n\nStudy a real module via `get_module {id}` (MCP) before writing a new one.\n",
3
+ "description": "Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk: the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route registration, and the shared controller-box bridge between an imperative scene and React panels. Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg, voxelcraft, mini-game, store, character, workshop…), or asks about defineModule, registerScene/registerPanel/registerRoute, ctx.features (register / open / setBadge / setAvailable), ctx.modes, activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic, ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content), ctx.multiplayer / registerGame (playing online through the lobby's rooms system), ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.",
4
+ "content": "---\nname: idosgames-module-contract\ndescription: >-\n Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk:\n the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route\n registration, and the shared controller-box bridge between an imperative scene and React panels.\n Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg,\n voxelcraft, mini-game, store, character, workshop…), or asks about defineModule,\n registerScene/registerPanel/registerRoute, ctx.features (register / open / setBadge / setAvailable),\n ctx.modes,\n activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic,\n ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content),\n ctx.multiplayer / registerGame (playing online through the lobby's rooms system),\n ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.\n---\n\n# The module contract (@idosgames/module-sdk)\n\nA module is a manifest the host installs once. It exports `{camelCase(id)}Module` from its\n`index.ts` (e.g. `board-game` → `boardGameModule`, `idle-rpg` → `idleRpgModule`) — the host seeder\nderives the import name from the id, so this convention is required.\n\n```ts\nimport { defineModule } from \"@idosgames/module-sdk\";\n\nexport const boardGameModule = defineModule({\n id: \"board-game\",\n meta: { name: \"Board Game\", type: \"game\", genre: \"board\", engine: \"three\" }, // engine: three|phaser|dom\n setup(ctx) {\n // ctx.client (shared, authed) · ctx.events (typed cross-module bus) · ctx.sharedUi · ctx.surface\n ctx.registerScene(createScene(box)); // rendered engine scene (optional)\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: RootPanel }); // React UI (optional)\n ctx.registerRoute({ id: \"board-game\", label: \"Board\", icon: \"🎲\" }); // a mode → the lobby's \"Play\"\n },\n});\n```\n\nTwo kinds of module plug into the base (`src/base/` — sign-in and the lobby):\n\n- a **game** (`meta.type: \"game\"`) registers a route: the lobby shows a \"Play\" button for it, and\n over it the base draws only a small \"To the lobby\" button;\n- a **system** (`meta.type: \"app\"`) registers a feature — its screen becomes a lobby tab:\n `ctx.features.register({ id, label, icon, group, order, primary?, available?, Screen })`. A tab\n that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows (details: **idosgames-ui-kit**).\n\n`ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type),\nfilled once all modules are installed (read it at render time, not inside `setup()`).\n`ctx.navigate(id)` switches (`\"lobby\"` is the base); called inside `setup()` it picks the mode the app\nstarts in. `ctx.features.open(id, args)` from a game (the lobby not on screen) switches to the lobby\nand opens that tab there.\n\n`ctx.multiplayer` — a game that plays online registers itself (`registerGame({ mode, topology,\nchatMode, maxPlayers, joinInProgress, settings, start(room) })`) and takes the room the lobby's\nrooms system hands it on \"Play\"; it never creates or joins rooms (skill **multiplayer-realtime**).\n\nOptional static field: `sharedUi: { provides?: SharedUiRole[]; requires?: SharedUiRole[] }` — see\n\"Shared chrome\" below.\n\n`meta.type` is `game | app | ai-app`; `meta.engine` is `three | phaser | dom` (`dom` = no renderer —\na pure React/DOM app module). `setup` is called ONCE with `ctx: ModuleContext`.\n\n## EngineScene (the rendered part)\n\nA scene mounts into a bare `HTMLElement` (framework-free — this is why a vanilla Three game like\nvoxelcraft fits). The host's Mode Router drives it; only the active mode runs.\n\n```ts\nconst scene: EngineScene = {\n surface: \"fullbleed-canvas\",\n mount(ctx: SceneMountContext) {\n controller = new Controller(ctx.host);\n box.set(controller);\n },\n activate() {\n controller?.setRunning(true);\n }, // became the active mode → resume RAF\n suspend() {\n controller?.setRunning(false);\n }, // hidden → stop RAF (invariant: only active ticks)\n destroy() {\n controller?.destroy();\n }, // permanent teardown\n};\n```\n\n`mount` takes a context object (not a bare element) so it can grow — e.g. a host-shared renderer in\nthe composition era — without breaking the contract. A scene MUST stop its RAF on `suspend`.\n\nA module that registers a scene MUST also publish its debug surface — `ctx.exposeToAgent({ state,\nactions, describeActions })` — and must NOT gate controls on Pointer Lock (unavailable in the\npreview's cross-origin iframe). Nothing inside a `<canvas>` is observable from the DOM, so without\nthe surface neither the AI Coder nor a human reviewer can tell what the game is doing. See the\n`idosgames-agent-debug-surface` skill.\n\n### `capture()` — the frame for \"Take a shot and share\"\n\nidosgames.com has a **Shot** button under the game: it asks the game for one frame, puts the\nplatform's branding and a QR code on it and offers to post it. A scene answers through the optional\n`capture?(): Promise<Blob | null>`. The host (`@idosgames/app-shell`) wires the request itself —\na module only has to return a picture of its active scene.\n\n**Implement it in any scene that renders to a canvas.** Without it the host falls back to reading\nthe `<canvas>` from outside, and for WebGL that is a trap: a renderer created WITHOUT\n`preserveDrawingBuffer` (the default, and the right default — the flag costs memory and a copy every\nframe) has already cleared its buffer by the time anyone reads it, so `toBlob()` returns a **fully\nblack** image and reports nothing. The host rejects single-colour frames, so the player gets the\ngame's cover instead of their moment.\n\nRender and read in the SAME tick:\n\n```ts\n// three.js\ncapture() {\n return new Promise((resolve) => {\n renderer.render(scene, camera); // draw now…\n renderer.domElement.toBlob(resolve, \"image/png\"); // …and read before the buffer is cleared\n });\n},\n\n// Phaser — use its own snapshot, it handles the timing\ncapture() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(null), 1500); // no frame came (context lost) → answer \"no\", don't hang\n game.loop.wake(); // a sleeping loop never draws again → snapshot never fires\n game.renderer.snapshot((image) => {\n if (!(image instanceof HTMLImageElement)) return resolve(null);\n const c = document.createElement(\"canvas\");\n c.width = image.naturalWidth || image.width;\n c.height = image.naturalHeight || image.height;\n c.getContext(\"2d\")?.drawImage(image, 0, 0);\n c.toBlob(resolve, \"image/png\");\n });\n });\n},\n```\n\nAlways answer — with a Blob or `null`. A `capture()` that never settles makes the site wait out its\ntimeout before it falls back, and the player stares at a spinner.\n\nReturn `null` when there is nothing worth showing (loading, no scene yet) — the site then uses the\ncover. Only the scene is captured, not the React panels over it: a shot for a feed reads better\nwithout the HUD. DOM-only modules (no canvas) simply omit `capture`. Working references:\n`modules/board-game` and `modules/voxelcraft` (three), `modules/idle-rpg` (Phaser).\n\n## UiPanel (the React part)\n\nPanels are React components the host renders inside its provider stack (client + status already\nprovided). Use `@idosgames/react` hooks (`useIDosGamesClient`, `useUserState`) — do NOT re-create\nproviders.\n\n- `slot`: `hud | sidebar | overlay | modal`.\n- `activeOnly` (default true): show only while this module's mode is active. Set `false` for shared\n chrome that stays across every mode (e.g. a persistent HUD).\n- `overlay`/`sidebar` layers sit between the shared HUD and the nav (the host measures both), so a\n panel positioned `absolute` inside its layer never ends up under them. Things drawn outside the\n layers use `var(--idos-safe-top, 0px)` / `var(--idos-safe-bottom, 0px)`.\n- A `hud` panel's wrapper is `display: contents`: the panel is a direct flex child of the HUD row\n (can `flex: 1`) and must set `pointer-events: auto` only on its controls.\n\n## Shared chrome (`sharedUi`)\n\nRoles: `currency-bar` (virtual-currency balances), `wallet` (crypto wallet entry — `LazyWalletPanel`),\n`status` (the `useStatus()` line), `account` (Log out + player ID), `menu` (the menu of features),\n`modes` (the switch between modes). The base takes `currency-bar`, `wallet`, `account`, `menu` and\n`modes` — its lobby draws them; `status` stays with each game. A module that draws one for every\nmode declares `sharedUi: { provides: [...] }` on the module object — statically, so the host resolves\nowners before any `setup()` and the answer never depends on module order.\n\n**Rule for templates: yield every role you draw yourself.** Read it once in `setup()` and close over\nthe answer (it is fixed for the session):\n\n```ts\nsetup(ctx) {\n const chrome = {\n wallet: ctx.sharedUi.shouldDraw(\"wallet\"),\n balances: ctx.sharedUi.shouldDraw(\"currency-bar\"),\n status: ctx.sharedUi.shouldDraw(\"status\"),\n };\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: makeRootPanel(box, chrome) });\n}\n```\n\nHide only the SHARED pieces — genre UI (the board's stage/tile/cycle chips) always stays. Never\nwrap your UI in your own `<StatusProvider>`: the host provides ONE, and a nested one would swallow\nyour messages so the shared status line never sees them. A module that needs a role but does not\ndraw it declares `sharedUi: { requires: [\"currency-bar\"] }`. `ctx.sharedUi.ownerOf(role)` names the\nprovider (or `null`). A full-screen mode (its own bottom tab bar) that shows the player ID and\n\"Log out\" in its own header declares `sharedUi: { drawsOwn: [\"account\"] }` and draws them only while\n`shouldDraw(\"account\")`: the host then hides its floating bottom-left copy while that mode is on\nscreen (it would cover the tab bar) and keeps it in the other modes. `drawsOwn` takes no role —\nwith the base installed the lobby draws the account and the mode hides its copy.\n\n## Events (`ctx.events`)\n\nTyped topic tokens — `defineTopic(\"<your-id>:<event>@1\", shape({ … }))` from\n`@idosgames/module-sdk`; emit only your own namespace; to listen to another module, copy its topic\nand payload descriptor from the catalog into your own `defineTopic` (never import it); declare both\nin `module.meta.json` (`events.emits` / `events.listens`). **Subscribe in `setup()`**, not in a panel\neffect — `activeOnly` panels unmount with their mode and miss events; store what arrives in a small\nstore the panel reads. Full rules and examples: **idosgames-compose-modules** (\"Signals between\nmodules\"). The string overloads (`emit(\"x\", …)`) are deprecated.\n\n## Content types (`ctx.content`) and switching modes (`ctx.navigate`)\n\nA game whose players make things (worlds, levels, skins) registers a **content type handler** so the\nshared Workshop module can publish and open them — the Workshop never imports the game:\n\n```ts\nsetup(ctx) {\n ctx.content.registerType({\n type: \"voxelcraft.world\", // = a key of Workshop.ContentTypes in the title config\n label: \"VoxelCraft world\",\n icon: \"⛏️\",\n modeId: \"voxelcraft\", // route to switch to after open()\n async listLocal() { return saves.map((s) => ({ id: s.id, name: s.name, updatedAt: s.at })); },\n async capture(localId) { // roles/MIME types must fit the title's content type config\n return { files: [{ role: \"main\", contentType: \"application/json\", data: json }],\n thumbnail: { contentType: \"image/webp\", data: webp }, suggestedTitle: name };\n },\n async open(content) { importSave(content.files[0].data); },\n });\n}\n```\n\nHandlers registered in `setup()` are removed with the module; `registerType` returns an unregister\nfunction for anything dynamic. `ctx.navigate(modeId)` switches the host to another module's route\n(the Workshop uses it to jump into the game after `open`); an unknown id is ignored. Details:\n**workshop-system**.\n\n## Bridging scene ↔ panel\n\nThe scene creates its controller at `mount` (needs the canvas), but panels render before that. Share\nit with a one-slot observable and read it with `useSyncExternalStore`:\n\n```ts\nexport function createControllerBox<T>() {\n /* get/set/subscribe */\n}\n// panel: const controller = useSyncExternalStore(box.subscribe, box.get); if (!controller) return <Loading/>;\n```\n\nFor the module's controller React context use the shared factory instead of hand-writing it:\n\n```ts\nexport const [BoardControllerProvider, useBoardController] =\n createControllerContext<BoardController>(\"BoardController\"); // from @idosgames/react\n```\n\n## Layout and boundaries\n\nA module is one folder, `src/modules/<id>/`. It imports only its own files, the game's shared code\nin `src/shared/` and npm packages — never another module's files or host files. Talk to other\nmodules through the shared `ctx.client` (durable state) and `ctx.events` (live signals, typed\ntopics `<module-id>:<event>@<major>` declared in `module.meta.json`). A module shipped in the catalog is fully self-contained — it never uses\n`src/shared/`, so it installs into any game; a game's own module may use it, and its shared code is\ncopied into it when it is published for other creators.\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ … })\n module.meta.json manifest: type (template|feature), summary, provides, tags, version, author\n components/ game/ data/ react/ — as needed\n```\n\nEvery module carries its own `module.meta.json` (the `ModuleManifest` shape), including a game's own\nmodules. The full standard — where a new feature goes, file size, documentation — is\n**idosgames-project-structure**.\n\n## Dependencies\n\nDeclare only the module's UNIQUE deps (e.g. `phaser` for idle-rpg, `three` for board/voxel) plus the\nshared baseline (react, @idosgames/*). The platform pins shared libs identically across modules; two\nmodules must not request different versions of one package (the build allowlist rejects it).\n\nStudy a real module via `get_module {id}` (MCP) before writing a new one.\n",
5
5
  "references": []
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idosgames-ui-kit",
3
- "description": "Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts), install a shop / heroes / inventory / leaderboard / rewards / craft / marketplace / offers / events / season / friends / chat / invite / arena / profile / tutorials / workshop system instead of writing it, open another module's screen with ctx.features.open, register a new screen (a lobby tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this whenever the user asks to change how the game's interface looks, sounds or animates, to add or build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features, FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.",
4
- "content": "---\nname: idosgames-ui-kit\ndescription: >-\n Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready\n system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts),\n install a shop / heroes / inventory / leaderboard / rewards / craft / marketplace / offers /\n events / season / friends / chat / invite / arena / profile / tutorials / workshop system instead of\n writing it, open another module's screen with ctx.features.open, register a new screen (a lobby\n tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a\n phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for\n tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this\n whenever the user asks to change how the game's interface looks, sounds or animates, to add or\n build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a\n settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features,\n FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.\n---\n\n# The UI kit and the system modules\n\nThe app's interface is the base's lobby (`src/base/`) plus **system modules**, all on one **UI kit**:\n\n- `@idosgames/react/ui` — the kit: components (buttons, tabs, popups, resource lists, item tiles,\n progress bars, timers), the reward popup, toasts, UI sounds (synthesized, no files), animations,\n and the player's own sound / motion prefs.\n- system modules — the platform's systems, each a whole `feature` module (its screen = a lobby\n tab, its client logic, its demo settings), all installed with the base into every new project\n (`inBase` in the catalog). The module id is the feature id:\n\n| Module | Screen |\n| ------------- | ---------------------------------------------------------------- |\n| `store` | shop: stores as tabs, offers, purchase popup |\n| `character` | heroes: gear slots, \"Equip best\", stats, ranks, skins (popups) |\n| `inventory` | items by rarity, item card, lootbox opening |\n| `leaderboard` | tables, my place, rank rewards, last cycle's prize |\n| `rewards` | daily calendar, quests, points track |\n| `craft` | recipes, contract popup |\n| `marketplace` | lots, auctions, selling, trades |\n| `deals` | special offers, auto-popup of a new deal |\n| `events` | limited-time events, milestones, VIP prize |\n| `season` | season tiers, VIP prize |\n| `friends` | friends, requests, suggestions, news |\n| `chat` | channels, private chats |\n| `invite` | own code and link, friend's code, rewards |\n| `pvp` | duels, strategy, fight replay |\n| `profile` | name, sound, language, consent, delete account |\n| `tutorials` | hint overlay + replay list |\n| `workshop` | players' and the publisher's content: catalog, publish, licenses |\n\n**Need one of these? Install the module — don't write the screen.** Every screen follows the\nserver's rules and ships demo title settings (`title-seed.json`). The app does not need one → remove it\nfrom `src/modules.ts`, its tab is gone. The lobby itself (tabs, balances, wallet, profile, \"Play\") is\nthe base, `src/base/lobby/`.\n\n## Change the look, animations and sounds — only in `src/ui.config.ts`\n\n```ts\nimport { defineUiConfig } from \"@idosgames/react/ui\";\n\nexport default defineUiConfig({\n theme: {\n bgTop: \"#10263f\",\n panel: \"#1b3a5c\",\n gold: \"#ffcc33\",\n radius: 12,\n rarity: { Mythic: \"#ff3df5\" },\n },\n motion: { speed: 1.5, intensity: 0.5, particles: false },\n sound: {\n volume: 0.4,\n events: {\n click: \"tab\",\n countTick: false,\n reward: { url: \"https://…/win.mp3\" },\n },\n },\n});\n```\n\n- Every field is optional; what you leave out keeps its default. The host passes the file to\n `mountHost({ ui })`, and every module on the kit follows it at once.\n- ⚠ **Never restyle a catalog module by editing it.** An edited module is marked \"customized\" and\n stops getting catalog updates. Colours, radii, fonts, animation speed and sounds are all here.\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `shadow`, `backdrop`, `radius`,\n `buttonRadius`, `font`, `rarity` (rarity id → frame colour).\n- Motion: `enabled`, `speed` (2 = twice as fast), `intensity` (0 = no bounce), `rewardFlight`,\n `particles`, `stagger`, `respectReducedMotion`.\n- Sound events: `click`, `tab`, `open`, `close`, `reward`, `coin`, `purchase`, `claim`, `error`,\n `notify`, `lootbox`, `levelUp`, `countTick`. Each: another event's name, `{ url }`, `{ synth: {\nnotes: [{ freq, dur, wave }] } }` or `false`.\n- The PLAYER's own sound / volume / \"fewer animations\" live on the device (the profile module draws\n them):\n `const [prefs, setPrefs] = usePlayerPrefs()` → `{ muted, volume, reducedMotion }`.\n\n## Features: screens that find each other\n\nA module registers a **feature**; whoever draws the menu shows it — the base's lobby as tabs (phone:\n4 + \"More\", tablet: more, desktop: a side rail; the base takes the shared-UI role `\"menu\"`), or the\nhost's floating ☰ when nobody takes the role. Tab order and main tabs can be overridden in\n`src/app.config.ts` (`lobby.order`, `lobby.primary`).\n\n```ts\n// module.ts\nsetup(ctx) {\n ctx.features.register({\n id: \"forge\", label: \"Forge\", icon: \"hammer\", group: \"economy\", order: 60,\n primary: false, // true = a main tab of the lobby\n Screen: ForgeScreen, // (props: FeatureScreenProps) => ReactNode; props.args, props.close()\n available: false, // hidden until the title has something for it — see below\n });\n // Badge (\"something to claim\"): ctx.features.setBadge(\"forge\", 2)\n // The title has forge recipes → show the tab; the reason is for the AI Coder's agent only:\n ctx.features.setAvailable(\"forge\", hasRecipes(configSection(ctx.client, \"Craft\")), \"no forge recipes\");\n}\n```\n\n- Open another module's screen without importing it: `ctx.features.open(\"store\")`,\n `ctx.features.open(\"chat\", { userID })`. It returns `false` when the module is not installed or its\n tab is hidden (`ctx.features.get(\"store\")?.available`). From a game (the lobby is off screen) the\n host shows the screen in a window.\n- A screen that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows — from the client config\n (`configSection(client, \"Store\")`) or a read it makes anyway (a badge watcher); no extra request\n at start-up: every module's first reads arrive together and brush the per-player rate limit.\n- Groups: `economy`, `events`, `social`, `profile`. Icons are kit glyph names (`shop`, `hero`,\n `bag`, `trophy`, `gift`, `hammer`, `market`, `offer`, `flame`, `friends`, `chat`, `link`,\n `swords`, `user`, `bulb`, …) or an image URL.\n- Something that must be on screen all the time (a daily auto-popup, a badge watcher, an overlay) is\n a panel: `ctx.registerPanel({ id, slot: \"modal\", activeOnly: false, component })`.\n\n## Building a screen on the kit\n\nKeep the screen a short list; put every detail in a **popup**.\n\n```tsx\nimport {\n Button,\n Popup,\n ResourceList,\n Tabs,\n useCelebrate,\n useToast,\n useUiKit,\n errorText,\n grantedBy,\n panel,\n outlined,\n v,\n} from \"@idosgames/react/ui\";\n\nfunction ForgeScreen(): ReactNode {\n const [open, setOpen] = useState<string | null>(null);\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n {/* rows… onClick={() => setOpen(id)} */}\n {open ? (\n <Popup title=\"Forge\" onClose={() => setOpen(null)} width={420}>\n …\n </Popup>\n ) : null}\n </div>\n );\n}\n```\n\n- `Popup` is a bottom sheet on a phone and a centred window elsewhere (`variant=\"center\"` forces\n centred); it animates in and out and plays open/close sounds. Close from inside with\n `usePopupClose()`. `ConfirmPopup` for yes/no. A popup that opens BY ITSELF (not on a tap) passes\n `queued` — auto-popups then show one at a time instead of stacking.\n- `useCelebrate()(lines)` shows \"You got\" and flies the icons to any element marked\n `data-resource-counter=\"{currencyID}\"` (the lobby's balances are). Build the lines\n from a response with `grantedBy(data)`; a reward with premium (VIP) bundles needs them added.\n- `useUiKit().play(\"claim\")`, `useUiKit().fx.burst(element)` — a sound and a particle burst.\n- `useToast()(text, \"error\")` for refusals; show `errorText(res.error)`, never re-check server rules.\n- Lines of resources: `grantLines(grant)`, `costLines(cost)`, `bundleLines(bundle)`,\n `canAfford(lines, balances)`, `<ResourceList lines={…} />`. Names and icons: `useCatalog()`.\n- Words: `makeT(EN, RU)` for your strings, `plural(n, \"лот|лота|лотов\")` for counts.\n- ⚠ A root `display: grid` with a wide child (a row of tabs) grows past a phone screen — give it\n `gridTemplateColumns: \"minmax(0, 1fr)\"`.\n\n## Tutorial hints\n\nThe `tutorials` module highlights any element marked `data-tutorial-anchor=\"…\"` — the lobby's tabs\nare `tab:{featureID}`, its profile button `lobby:profile`, its wallet `lobby:wallet`, a game's \"Play\"\nbutton `play:{modeID}`. Mark the controls of your own screen the same\nway; the publisher points tutorial steps at them in LiveOps (Tutorial → step → AnchorID).\n",
3
+ "description": "Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts), install a shop / VIP / heroes / inventory / chests / boosts / leaderboard / daily rewards / quests / collections / craft / marketplace / offers / events / team event / season / friends / chat / invite / arena / multiplayer rooms / profile / tutorials / workshop / AI studio system instead of writing it, open another module's screen with ctx.features.open, register a new screen (a lobby tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this whenever the user asks to change how the game's interface looks, sounds or animates, to add or build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features, FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.",
4
+ "content": "---\nname: idosgames-ui-kit\ndescription: >-\n Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready\n system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts),\n install a shop / VIP / heroes / inventory / chests / boosts / leaderboard / daily rewards / quests /\n collections / craft / marketplace / offers / events / team event / season / friends / chat / invite /\n arena / multiplayer rooms / profile / tutorials / workshop / AI studio system instead of writing it, open another module's screen with ctx.features.open, register a new screen (a lobby\n tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a\n phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for\n tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this\n whenever the user asks to change how the game's interface looks, sounds or animates, to add or\n build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a\n settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features,\n FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.\n---\n\n# The UI kit and the system modules\n\nThe app's interface is the base's lobby (`src/base/`) plus **system modules**, all on one **UI kit**:\n\n- `@idosgames/react/ui` — the kit: components (buttons, tabs, popups, resource lists, item tiles,\n progress bars, timers), the reward popup, toasts, UI sounds (synthesized, no files), animations,\n and the player's own sound / motion prefs.\n- system modules — the platform's systems, each a whole `feature` module (its screen = a lobby\n tab, its client logic, its demo settings). The base installs six of them into every new project\n (`inBase`: profile, inventory, invite, friends, quests, mailbox); the rest are installed only when the app\n needs them (a game's `uses`, or a task that needs what they `provide`). ONE system per player-facing module of the engine\n (`API/Client/v2/<Module>`) — the owner's rule since 29.09.2026. The module id is the feature id:\n\n| Module | Engine module | Screen |\n| ------------- | -------------- | ------------------------------------------------------------- |\n| `store` | Store+Purchase | shop: stores as tabs, offers, purchase popup |\n| `premium` | Premium | VIP passes: benefits, timer, free trial, buy / extend |\n| `character` | Character | heroes: gear slots, \"Equip best\", stats, ranks, skins |\n| `inventory` | Item | items by rarity, item card (always shown, even with no items) |\n| `lootboxes` | Lootbox | chests: every way to open, ×1 / ×10, opening animation |\n| `boosts` | TimedBoost | running boosts + global happy hours, activate a boost |\n| `leaderboard` | Leaderboard | tables, my place, rank rewards, last cycle's prize |\n| `rewards` | Reward | daily login calendar, greeting popup |\n| `quests` | Quest | quests by cycle, details, claim all, points track |\n| `collections` | Collection | album of sets, packs, chests for points, card trades |\n| `craft` | Craft | recipes, contract popup |\n| `marketplace` | Marketplace | lots, auctions, selling, trades |\n| `deals` | DealOffer | special offers, auto-popup of a new deal |\n| `events` | TimedEvent | limited-time events, milestones, VIP prize |\n| `coop-events` | CoopEvent | team event: join a group, spin to build, team prize |\n| `season` | Season | season tiers, VIP prize |\n| `friends` | Social | friends, requests, suggestions, news |\n| `chat` | Chat | channels, private chats |\n| `invite` | Referral | own code and link, friend's code, rewards (always shown) |\n| `pvp` | Match | duels, strategy, fight replay |\n| `multiplayer` | Multiplayer | rooms: list, create / join by code, ready, chat, host \"Play\" |\n| `profile` | User | name, sound, language, consent, delete account |\n| `tutorials` | Tutorial | hint overlay + replay list |\n| `workshop` | Workshop | players' and the publisher's content: catalog, publish |\n| `ai` | AI | AI studio: the title's AI features for players, history |\n\nNot a system on purpose: Currency, Authentication, Blockchain (the base — balances, sign-in,\nwallet); GameLoop (the `board-game` game); CommunityMarketing (idosgames.com only); Mailbox and\nAdvertising (no client handler / no web ad network); Analytics, Experiment, Push, Localization,\nCloudCode, DataCollections, TitleCustomData, UserCustomData (services the systems and games call).\n\nHand-offs between systems go through `ctx.features.open(id, args)`: inventory → `character`\n(`{ itemInstanceID }`) and → `lootboxes` (`{ itemID }` of a chest or key); events / season →\n`premium` (`{ premiumID }`). Multiplayer: the `multiplayer` system owns the rooms; a game registers\nin `ctx.multiplayer` and takes the room in `start(room)` (skill **multiplayer-realtime**); the lobby\nshows \"With friends\" next to such a game's \"Play\".\n\n**Need one of these? Install the module — don't write the screen.** Every screen follows the\nserver's rules and ships demo title settings (`title-seed.json`). The app does not need one → remove it\nfrom `src/modules.ts`, its tab is gone. The lobby itself (tabs, balances, wallet, profile, \"Play\") is\nthe base, `src/base/lobby/`.\n\n## Change the look, animations and sounds — only in `src/ui.config.ts`\n\n```ts\nimport { defineUiConfig } from \"@idosgames/react/ui\";\n\nexport default defineUiConfig({\n theme: {\n bgTop: \"#10263f\",\n panel: \"#1b3a5c\",\n gold: \"#ffcc33\",\n radius: 12,\n rarity: { Mythic: \"#ff3df5\" },\n },\n motion: { speed: 1.5, intensity: 0.5, particles: false },\n sound: {\n volume: 0.4,\n events: {\n click: \"tab\",\n countTick: false,\n reward: { url: \"https://…/win.mp3\" },\n },\n },\n});\n```\n\n- Every field is optional; what you leave out keeps its default. The host passes the file to\n `mountHost({ ui })`, and every module on the kit follows it at once.\n- ⚠ **Never restyle a catalog module by editing it.** An edited module is marked \"customized\" and\n stops getting catalog updates. Colours, radii, fonts, animation speed and sounds are all here.\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `shadow`, `backdrop`, `radius`,\n `buttonRadius`, `font`, `rarity` (rarity id → frame colour).\n- Motion: `enabled`, `speed` (2 = twice as fast), `intensity` (0 = no bounce), `rewardFlight`,\n `particles`, `stagger`, `respectReducedMotion`.\n- Sound events: `click`, `tab`, `open`, `close`, `reward`, `coin`, `purchase`, `claim`, `error`,\n `notify`, `lootbox`, `levelUp`, `countTick`. Each: another event's name, `{ url }`, `{ synth: {\nnotes: [{ freq, dur, wave }] } }` or `false`.\n- The PLAYER's own sound / volume / \"fewer animations\" live on the device (the profile module draws\n them):\n `const [prefs, setPrefs] = usePlayerPrefs()` → `{ muted, volume, reducedMotion }`.\n\n## Features: screens that find each other\n\nA module registers a **feature**; whoever draws the menu shows it — the base's lobby as tabs (phone:\n4 + \"More\", tablet: more, desktop: a side rail; the base takes the shared-UI role `\"menu\"`), or the\nhost's floating ☰ when nobody takes the role. Tab order and main tabs can be overridden in\n`src/app.config.ts` (`lobby.order`, `lobby.primary`).\n\n```ts\n// module.ts\nsetup(ctx) {\n ctx.features.register({\n id: \"forge\", label: \"Forge\", icon: \"hammer\", group: \"economy\", order: 60,\n primary: false, // true = a main tab of the lobby\n Screen: ForgeScreen, // (props: FeatureScreenProps) => ReactNode; props.args, props.close()\n available: false, // hidden until the title has something for it — see below\n });\n // Badge (\"something to claim\"): ctx.features.setBadge(\"forge\", 2)\n // The title has forge recipes → show the tab; the reason is for the AI Coder's agent only:\n ctx.features.setAvailable(\"forge\", hasRecipes(configSection(ctx.client, \"Craft\")), \"no forge recipes\");\n}\n```\n\n- Open another module's screen without importing it: `ctx.features.open(\"store\")`,\n `ctx.features.open(\"chat\", { userID })`. It returns `false` when the module is not installed or its\n tab is hidden (`ctx.features.get(\"store\")?.available`). From a game (the lobby is off screen) the\n host shows the screen in a window.\n- A screen that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows — from the client config\n (`configSection(client, \"Store\")`) or a read it makes anyway (a badge watcher); no extra request\n at start-up: every module's first reads arrive together and brush the per-player rate limit.\n- Groups: `economy`, `events`, `social`, `profile`. Icons are kit glyph names (`shop`, `hero`,\n `bag`, `trophy`, `gift`, `hammer`, `market`, `offer`, `flame`, `friends`, `chat`, `link`,\n `swords`, `user`, `bulb`, …) or an image URL.\n- Something that must be on screen all the time (a daily auto-popup, a badge watcher, an overlay) is\n a panel: `ctx.registerPanel({ id, slot: \"modal\", activeOnly: false, component })`.\n\n## Building a screen on the kit\n\nKeep the screen a short list; put every detail in a **popup**.\n\n```tsx\nimport {\n Button,\n Popup,\n ResourceList,\n Tabs,\n useCelebrate,\n useToast,\n useUiKit,\n errorText,\n grantedBy,\n panel,\n outlined,\n v,\n} from \"@idosgames/react/ui\";\n\nfunction ForgeScreen(): ReactNode {\n const [open, setOpen] = useState<string | null>(null);\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n {/* rows… onClick={() => setOpen(id)} */}\n {open ? (\n <Popup title=\"Forge\" onClose={() => setOpen(null)} width={420}>\n …\n </Popup>\n ) : null}\n </div>\n );\n}\n```\n\n- `Popup` is a bottom sheet on a phone and a centred window elsewhere (`variant=\"center\"` forces\n centred); it animates in and out and plays open/close sounds. Close from inside with\n `usePopupClose()`. `ConfirmPopup` for yes/no. A popup that opens BY ITSELF (not on a tap) passes\n `queued` — auto-popups then show one at a time instead of stacking.\n- `useCelebrate()(lines)` shows \"You got\" and flies the icons to any element marked\n `data-resource-counter=\"{currencyID}\"` (the lobby's balances are). Build the lines\n from a response with `grantedBy(data)`; a reward with premium (VIP) bundles needs them added.\n- `useUiKit().play(\"claim\")`, `useUiKit().fx.burst(element)` — a sound and a particle burst.\n- `useToast()(text, \"error\")` for refusals; show `errorText(res.error)`, never re-check server rules.\n- Lines of resources: `grantLines(grant)`, `costLines(cost)`, `bundleLines(bundle)`,\n `canAfford(lines, balances)`, `<ResourceList lines={…} />`. Names and icons: `useCatalog()`.\n- Words: `makeT(EN, RU)` for your strings, `plural(n, \"лот|лота|лотов\")` for counts.\n- ⚠ A root `display: grid` with a wide child (a row of tabs) grows past a phone screen — give it\n `gridTemplateColumns: \"minmax(0, 1fr)\"`.\n\n## Tutorial hints\n\nThe `tutorials` module highlights any element marked `data-tutorial-anchor=\"…\"` — the lobby's tabs\nare `tab:{featureID}`, its profile button `lobby:profile`, its wallet `lobby:wallet`, a game's \"Play\"\nbutton `play:{modeID}`. Mark the controls of your own screen the same\nway; the publisher points tutorial steps at them in LiveOps (Tutorial → step → AnchorID).\n",
5
5
  "references": []
6
6
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "mailbox-system",
3
+ "description": "Build a mailbox / inbox in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.mailbox (MailboxService): letters with rewards from the publisher (personal letters and scheduled broadcasts), free daily gifts between friends, transfers of a player's own resources to a friend (declined or expired ones come back), and rewards of other systems the player has not claimed yet (leaderboard cycle, team event prize, invite rewards, missed season tiers). Claim one letter or \"Claim all\" in one server operation, mark read, delete, the badge counters. Use this whenever the user wants a mailbox, inbox, mail, letters, compensation / gift letters, \"send a gift to a friend\", \"claim all\", a mail badge, or touches client.mailbox, MailboxService, MailboxDefinitions, MailboxLetter, MailboxPendingReward — even if they don't name the module. The ready screen is the `mailbox` module (modules/mailbox).",
4
+ "content": "---\nname: mailbox-system\ndescription: >-\n Build a mailbox / inbox in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.mailbox (MailboxService): letters with rewards\n from the publisher (personal letters and scheduled broadcasts), free daily\n gifts between friends, transfers of a player's own resources to a friend\n (declined or expired ones come back), and rewards of other systems the\n player has not claimed yet (leaderboard cycle, team event prize, invite\n rewards, missed season tiers). Claim one letter or \"Claim all\" in one\n server operation, mark read, delete, the badge counters.\n Use this whenever the user wants a mailbox, inbox, mail, letters,\n compensation / gift letters, \"send a gift to a friend\", \"claim all\", a\n mail badge, or touches client.mailbox, MailboxService, MailboxDefinitions,\n MailboxLetter, MailboxPendingReward — even if they don't name the module.\n The ready screen is the `mailbox` module (modules/mailbox).\n---\n\n# Mailbox system (iDosGames TS SDK)\n\nThe mailbox holds **value the player claims**. It is not a notification feed\n(that is Push) and not the social activity inbox (`client.social.getInbox`).\nEverything is **server-authoritative**: expiry, limits, friendship, what may be\ntransferred and every grant are the engine's. The client reads pages, calls an\naction, and renders the result; the SDK applies granted/charged resources to\nthe local cache itself.\n\nA ready screen exists: the **`mailbox` module** (tabs by category, letter popup,\nClaim / Claim all, \"Send gifts\", badge). Compose it instead of writing a new one\nunless the game needs its own look. Open it on a friend from anywhere:\n\n```ts\nctx.features.open(\"mailbox\", { friendUserID, mode: \"gift\" }); // or mode: \"transfer\"\n```\n\n## Key entities\n\n1. **`MailboxDefinitions`** — the title's `Mailbox` config section:\n `Enabled` (off by default — no mailbox, no tab), capacity and letter\n lifetimes, `Templates` (publisher texts per language), `Gifts.Social`\n (free gifts: `Grant`), `Gifts.Transfer` (`Enabled`, `AllowedResources` with\n `MaxAmountPerGift`), `Integrations` (which systems may send missed rewards).\n The client copy has NO daily limits, friendship threshold or sender gates —\n those are anti-fraud limits kept on the server; ask `getGiftStatus()`.\n2. **`MailboxLetter`** — one stored letter: `MessageID`, `Source`\n (Admin, Broadcast, SocialGift, Transfer, TransferReturn, Module, CloudCode),\n `Category` (System, Reward, Gift, Social), `Subject`/`Body` already in the\n requested language, `TemplateID` + `TemplateParams`, `Icon`, `Banner`,\n `Link` (`FeatureID` → `ctx.features.open`, or `Url`), `SenderName`,\n `SenderNote`, `Rewards` (a `ResourceGrant`), `HasRewards`, `IsTransfer`,\n `SentAt`, `ExpiresAt`, `ReadAt`, `ClaimedAt`.\n3. **`MailboxPendingReward`** — a LIVE reward of another system\n (`MessageID = \"live:{Module}:{Key}\"`). The mailbox does not store it: the\n module itself grants it, so claiming it here or on the module's screen is\n the same claim — never both.\n\n⚠ **`Subject`/`Body` can be null.** Letters of the platform's own templates\n(`system.gift`, `system.transfer`, `system.transfer_return`,\n`system.leaderboard_reward`, `system.coop_grand_prize`,\n`system.coop_object_reward`, `system.season_unclaimed`,\n`system.referral_reward`) come without text unless the publisher rewrote the\ntemplate. Render them from your own strings by `TemplateID`, filling\n`{param}` placeholders from `TemplateParams` (`sender`, `rank`, `leaderboard`,\n`event`, `season`, `tier`, `friends`).\n\n## Methods\n\nAll return `Promise<OperationResult<T>>` — branch on `ok`. Codes come back in\n`error` as plain strings (`MailboxErrors`: `ALREADY_CLAIMED`, `EXPIRED`,\n`NOT_FRIENDS`, `ALREADY_SENT`, `SEND_LIMIT`, `RECIPIENT_LIMIT`, `NOT_ENOUGH_RESOURCES`,\n`AMOUNT_TOO_SMALL`, `FRIENDSHIP_TOO_NEW`,\n`AMOUNT_TOO_LARGE`, `RESOURCE_NOT_GIFTABLE`, …) — map them to words.\n\n| Method | What it does |\n| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `getMessages({ cursor?, pageSize?, category?, language? })` | One page, newest first. The FIRST page (no cursor) also delivers broadcasts meant for this player, returns their own expired transfers, and carries `PendingRewards`, `UnreadCount`, `UnclaimedCount`. Language defaults to the client locale. |\n| `getUnreadCount()` | Badge counters only; writes nothing — safe for a menu badge. |\n| `readMessages(ids \\| \"all\")` | Mark read. |\n| `claimReward(messageID)` | One letter or one `live:` reward. |\n| `claimRewards(ids)` | Several in one atomic operation; failures come back in `Failed`. |\n| `claimAll()` | Everything claimable, letters and live rewards. |\n| `deleteMessages(ids \\| \"allRead\", { force? })` | `\"allRead\"` keeps letters with unclaimed rewards. Deleting a pending TRANSFER declines it — the resources go back to the friend. |\n| `sendGift(giftID, recipientUserIDs)` | Gift, once per period per friend (a day unless the title set `IntervalSeconds` from `AnchorUtc`). A gift with `Cost` charges the sender per friend (applied locally from `Resources`). `Sent` / `Failed` per friend. |\n| `sendTransfer(friendID, entries, { requestID, note? })` | Own resources, charged NOW, wait in the friend's letter. |\n| `getGiftStatus()` | Remaining limits of the current period (`RemainingInPeriod`, `ResetsAt`) and who already got each gift in it (`SentTo`). |\n\nClaims return ONE merged `Resources` for the whole request and the service\napplies it once — never add up the per-letter `Claimed[].Rewards` yourself.\n\n⚠ **`requestID` of a transfer is its idempotency key.** Generate one per\nattempt and keep it for retries of the SAME transfer (a timeout then replays\ninstead of charging twice — `IdempotentReplay: true`). Make a new one only after\na success.\n\n## Badge\n\nEvery mailbox response carries fresh counters and the service emits\n`mailbox:countersChanged` (`{ UnreadCount, UnclaimedCount }`). Read\n`getUnreadCount()` once on start (and when the player returns to the tab), then\njust listen — no polling. Counters are capped at `MAILBOX_COUNTER_CAP` (100):\nshow \"99+\". Unread and unclaimed overlap (a new letter with a gift is both) —\nshow the bigger one, not the sum.\n\n```ts\nconst off = client.on(\"mailbox:countersChanged\", (c) =>\n setBadge(Math.max(c.UnreadCount, c.UnclaimedCount)),\n);\nvoid client.mailbox.getUnreadCount();\n```\n\nOther events: `mailbox:messagesLoaded`, `mailbox:rewardsClaimed`,\n`mailbox:giftSent`, `mailbox:transferSent`.\n\n## Recipes\n\n### Inbox with \"Claim all\"\n\n```ts\nconst page = await client.mailbox.getMessages();\nif (!page.ok) return showError(page.error);\nrender(page.data.Messages, page.data.PendingRewards ?? []);\n\nconst all = await client.mailbox.claimAll();\nif (all.ok) celebrate(all.data.Resources); // balances already updated\n```\n\nPaging: pass `NextCursor` back as `cursor`; stop when it is null. Letters of a\nlater page never repeat earlier ones, but de-duplicate by `MessageID` anyway if\nthe list was refreshed in between.\n\n### Gift every friend who has not got one today\n\n```ts\nconst status = await client.mailbox.getGiftStatus();\nconst sent = new Set(\n status.ok ? (status.data.Social[giftID]?.SentTo ?? []) : [],\n);\nconst friends = await client.social.getFriendsList();\nconst open = (friends.ok ? (friends.data.Friends ?? []) : [])\n .map((f) => f.UserID)\n .filter((id) => !sent.has(id));\nawait client.mailbox.sendGift(giftID, open);\n```\n\n### Transfer coins to a friend\n\n```ts\nconst requestID = crypto.randomUUID(); // keep it for retries of THIS transfer\nconst r = await client.mailbox.sendTransfer(\n friendID,\n [{ Type: \"VirtualCurrency\", CurrencyID: \"CO\", Amount: 100 }],\n { requestID, note: \"good luck\" },\n);\n```\n\nOnly resources of `Gifts.Transfer.AllowedResources` can go — `MaxAmountPerGift` is a CAP (any\namount from 1 up to it), entries go separately or together up to `MaxEntriesPerGift`. `FeePercent`\nis withheld from what the friend gets (show `RecipientReceives`), `FeeCost` is charged on top; both\nburn only when the friend claims — a declined or expired transfer returns everything charged; unstackable item instances are never transferable. Friends\nmust be friends long enough (`FRIENDSHIP_TOO_NEW`) — the threshold is not in\nthe client config.\n\n## Gotchas\n\n- **Mailbox off = no calls.** With `Enabled !== true` every action fails with\n `MAILBOX_DISABLED`; hide the tab instead.\n- **Crypto currency never travels by mail** — neither in letters nor\n transfers (`CRYPTO_FORBIDDEN`).\n- **A letter expires** (`ExpiresAt`): its reward can no longer be claimed; a\n transfer that expires goes back to the sender automatically.\n- **Sending letters is not a client action.** Letters come from the publisher\n dashboard (personal letters, broadcasts by segment and schedule), from\n CloudCode (`server.SendMail`), or from the engine's own systems. A client\n can only gift and transfer between friends.\n",
5
+ "references": []
6
+ }