@idosgames/mcp 0.1.1 → 0.1.3
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.
- package/dist/cli.js +6 -3
- package/package.json +1 -1
- package/registry/host.json +14 -6
- package/registry/index.json +32 -26
- package/registry/modules/board-game.json +12 -15
- package/registry/modules/idle-rpg.json +14 -17
- package/registry/modules/voxelcraft.json +28 -24
- package/registry/skills/blockchain-system.json +1 -1
- package/registry/skills/cloud-code.json +2 -2
- package/registry/skills/idosgames-agent-debug-surface.json +6 -0
- package/registry/skills/idosgames-getting-started.json +1 -1
- package/registry/skills/idosgames-module-contract.json +1 -1
- package/registry/skills/idosgames-title-bootstrap.json +6 -0
- package/registry/skills/quest-system.json +3 -3
- package/registry/skills/title-custom-data.json +6 -0
- package/registry/skills/title-system.json +2 -2
- package/registry/skills/user-custom-data.json +1 -1
|
@@ -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### 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`/`WithdrawFee`, `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\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`/`WithdrawFee`, `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": "cloud-code",
|
|
3
|
-
"description": "
|
|
4
|
-
"content": "---\nname: cloud-code\ndescription: >-\n Call custom server-side game logic on the iDosGames TypeScript SDK\n (@idosgames/core) via client.cloudCode (CloudCodeService): execute a\n title-defined cloud script by name with an arbitrary JSON args payload and\n get back its arbitrary JSON result. Use this whenever the user wants to run\n custom/bespoke server logic, a \"cloud script\", \"cloud function\", \"server\n callable\", crafting/trading/matchmaking logic not covered by a dedicated SDK\n module, or otherwise touches client.cloudCode, CloudCodeService, or\n ExecuteCloudCodeResponse — even if they don't name the module explicitly.\n---\n\n# Cloud Code (iDosGames TS SDK)\n\nCloudCodeService is the **escape hatch**: a generic way to run a title-defined\nserver-side script (a \"handler\") and get back whatever JSON that script\nreturns. Use it when a feature doesn't have a dedicated SDK module (Character,\nItem, Economy, etc.) — e.g. bespoke crafting rules, custom matchmaking, an\nadmin action, anything that's easier to write once as server logic than to\ncompose from generic client calls. If a dedicated module already covers what\nyou need, prefer that module — it gives you typed request/response shapes and\ncache integration; Cloud Code gives you neither.\n\nThis skill is for **using** production Cloud Code, not for writing the scripts\nthemselves (that's title/server-side configuration — a JavaScript revision\ndeployed and administered outside this SDK, out of scope here).\n\n## Mental model\n\nThere is exactly one client-facing action: `execute`. You pass a **function\nname** (a handler defined in the title's deployed script's `handlers` object)\nand an optional **arbitrary JSON payload**; the server runs it in a sandboxed\nJS engine and returns an arbitrary JSON result plus execution metadata (logs,\ntiming, error info). The SDK has no idea what a given script's args or result\nlook like — **you** know your title's script contract, so you type the\npayload and result yourself (see Gotchas). There's no \"config vs state\" split\nhere like other modules — Cloud Code has no persistent per-player data model\nof its own; it's pure request/response.\n\nScript failure is a **first-class outcome, not a network error**: if the\nscript throws, times out, is rate-limited, or the handler doesn't exist, the\ncall still comes back `{ ok: true, data }` with `data.Error` populated and\n`data.FunctionResult` empty. Only infrastructure problems (not logged in, bad\nlocal args, connection issues, backend down) surface as `{ ok: false }`.\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 cloudCode = client.cloudCode; // the CloudCodeService\n```\n\nRequires an authenticated session — without one, `execute` returns\n`{ ok: false, reason: \"unauthorized\" }` rather than making a request.\n\n## Methods\n\n`execute` returns `Promise<OperationResult<ExecuteCloudCodeResponse>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data` — and then check `data.Error` before\ntrusting `data.FunctionResult` (see below). `reason` is one of `\"client\"`\n(empty/whitespace-only function name), `\"unauthorized\"`, `\"throttled\"` (fired\nthe same call again inside the throttle window), `\"connection\"` (transient,\noffer Retry), `\"validation\"` (response/schema drift), or `\"server\"`\n(infrastructure-level rejection — `error` carries the human-readable reason).\n\n| Method | Purpose | `data` on success |\n| ---------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------- |\n| `execute(functionName, functionParameter?, revisionSelection?, specificRevision?)` | Run a title-defined cloud script handler by name. | `ExecuteCloudCodeResponse` |\n\nParameters:\n\n- `functionName` — the handler name inside the deployed script's `handlers`\n object. Case-sensitive; trimmed before sending. Client-side, only\n empty/whitespace is rejected (`reason: \"client\"`). Server-side, the backend\n additionally rejects (as a script-level `InvalidFieldName` error, not an\n `OperationResult` failure) names containing `.`, `$`, whitespace, or control\n characters, or longer than 128 characters — these are illegal as MongoDB\n field names since the name can end up in audit/log paths.\n- `functionParameter?` — any `JsonValue` (object, array, string, number,\n boolean, or null) passed as the handler's first argument. Omit if the script\n needs no input. If it's an object (at any nesting depth), none of its keys\n may contain `.` or `$` — the backend rejects such payloads with a\n script-level `InvalidFieldName` error before the script ever runs.\n- `revisionSelection?` — `\"Live\"` (default when omitted), `\"Latest\"`, or\n `\"Specific\"`. Lets you target a non-live revision for testing.\n- `specificRevision?` — the revision number to run; only used when\n `revisionSelection` is `\"Specific\"`.\n\n`ExecuteCloudCodeResponse` shape:\n\n| Field | Meaning |\n| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |\n| `FunctionName` | Echo of the handler that ran. |\n| `Revision` | Which revision actually executed. |\n| `FunctionResult` | The script's return value — arbitrary JSON, `null` if it returned nothing or on error. |\n| `FunctionResultTooLarge` | `true` if the result was dropped for exceeding the title's result-size limit (`FunctionResult` is `null` in that case). |\n| `Logs` | Array of `{ Level, Message?, Data? }` entries from `log.debug/info/warn/error` calls inside the script. |\n| `LogsTooLarge` | `true` if logs were truncated for exceeding the title's log-size limit. |\n| `ExecutionTimeSeconds` | Server-side wall-clock execution duration. |\n| `APIRequestsIssued` | Count of server API calls the script made internally (e.g. reading user data) — counts toward a per-execution cap. |\n| `Error` | `{ Error: CloudCodeErrorCode, Message?, StackTrace? }`, present only when the script failed or never ran; `null`/absent on success. |\n\n`CloudCodeErrorCode` values: `None`, `Disabled`, `NoActiveRevision`,\n`RevisionNotFound`, `InvalidFieldName`, `RateLimited`, `HandlerNotFound`,\n`HandlerDisabled`, `Timeout`, `StatementCountExceeded`, `StackOverflow`,\n`ApiCallLimitExceeded`, `JavaScriptException`, `ExecutionError` — stable, safe\nto switch on for retry/UX logic (e.g. treat `RateLimited`/`Timeout` as\nretryable, others as not).\n\nOn success, the SDK emits an event — it does **not** write anything into\n`client.data`, since the result shape is script-specific and there's no\ngeneric cache slot for it. If your script mutates player state (grants\ncurrency, items, etc. via server-side APIs), re-fetch that state through its\nowning module afterward — Cloud Code itself won't refresh your local cache.\n\n## Events\n\nSubscribe with `client.on(...)`; returns an unsubscribe fn.\n\n- `cloudCode:executed` → `ExecuteCloudCodeResponse` — fired whenever `execute` returns `{ ok: true }`, regardless of whether the script itself succeeded (check `data.Error` inside the handler).\n\n```ts\nconst off = client.on(\"cloudCode:executed\", (r) => {\n if (r.Error) console.warn(\"script failed:\", r.Error.Error, r.Error.Message);\n});\n// later: off();\n```\n\n## Recipes\n\n### Call a script and handle both failure layers\n\n```ts\ninterface GrantBonusArgs {\n reason: string;\n}\ninterface GrantBonusResult {\n granted: number;\n}\n\nconst args: GrantBonusArgs = { reason: \"daily\" };\nconst result = await client.cloudCode.execute(\"grantLoginBonus\", args);\nif (!result.ok) return showError(result.error ?? result.reason); // infra-level failure\n\nif (result.data.Error) {\n return showError(result.data.Error.Message ?? result.data.Error.Error); // script-level failure\n}\n\nconst payload = result.data.FunctionResult as GrantBonusResult; // your contract — cast/validate it yourself\nconsole.log(`granted ${payload.granted}`);\n```\n\n### Fire-and-forget script with no input\n\n```ts\nconst result = await client.cloudCode.execute(\"resetDailyQuests\");\nif (!result.ok || result.data.Error) {\n console.warn(\"resetDailyQuests failed\", result.error ?? result.data.Error);\n}\n```\n\n### Test against a specific revision before it goes live\n\n```ts\nconst result = await client.cloudCode.execute(\n \"computeMatchReward\",\n { matchID },\n \"Specific\",\n 42, // revision number\n);\n```\n\n### Surface script logs during development\n\n```ts\nconst result = await client.cloudCode.execute(\"debugScript\", { x: 1 });\nif (result.ok) {\n for (const log of result.data.Logs ?? []) {\n console.log(`[${log.Level}]`, log.Message, log.Data);\n }\n}\n```\n\nLogs only come back at all if the title has logs enabled for clients; on\ntitles that don't, `Logs` is always an empty array even though the script did\nlog server-side — don't treat an empty array as proof the script logged\nnothing.\n\n### Chain a cloud-code call with a resource refresh\n\n```ts\nconst res = await client.cloudCode.execute(\"craftSpecialItem\", { recipeID });\nif (!res.ok || res.data.Error) return showError(res.error ?? res.data.Error);\n\n// The script granted items/currency server-side — Cloud Code didn't touch the\n// cache, so pull the owning module's state to see the new balance/inventory.\nawait client.user.getClientState(); // or the specific module's getter, e.g. client.item...\n```\n\n## Gotchas\n\n- **Two failure layers, don't conflate them.** `result.ok === false` means the\n call itself failed (auth, bad args, connection) — the script never ran or\n its outcome is unknown. `result.ok === true && result.data.Error` means the\n call succeeded but the _script_ failed (threw, timed out, disabled,\n unknown/undeclared handler, rate-limited) — always check both before\n trusting `FunctionResult`.\n- **Unknown handler is a script-level error, not a client-side check.** The\n SDK never validates that `functionName` refers to a real handler — that's\n entirely server-side. Depending on the title's config you can get\n `HandlerNotFound` either because the name isn't in the title's declared\n handler whitelist, or because the deployed script simply never defined\n `handlers[functionName]`; both look the same to the caller. A handler can\n also be individually killed by an admin, which comes back as\n `HandlerDisabled`.\n- **A hard 10-second ceiling always applies.** Whatever timeout the title/\n revision configures, the backend clamps every single execution to a 10\n second wall-clock budget; past that you get `Timeout` no matter what. Don't\n design a script-based feature around long-running work.\n- **Rate limiting can hit independently of the generic per-endpoint throttle.**\n Beyond the SDK's own ~600ms client-side throttle per call and the\n transport's per-user rate limit, the title can configure CloudCode-specific\n limits at three levels — whole title, this user, or this user+handler pair.\n Any of them tripping comes back as `data.Error.Error === \"RateLimited\"`\n (an in-band script-level outcome, `result.ok` is still `true`), with\n `data.Error.Message` naming which layer triggered it — treat it as\n retryable-after-a-delay, not a hard failure.\n- **No client-side validation of script logic.** The SDK only validates that\n `functionName` is non-empty and that you're logged in. Argument shape,\n business rules, and error handling are entirely up to the script — a\n malformed `functionParameter` will fail server-side (`JavaScriptException`\n or similar), not client-side.\n- **Type the payload and result yourself.** `functionParameter` is `JsonValue`\n and `FunctionResult` is `JsonValue | null` — the SDK has no schema for your\n title's specific scripts. Define your own request/response interfaces per\n handler (as in the recipes above) and cast/validate after the call.\n- **Cloud Code doesn't touch `client.data`.** Unlike feature modules, a\n successful `execute` doesn't mirror anything into the cache. If the script\n changed player-facing state, re-fetch it via the owning module (e.g. call\n the Economy/Item/Character module's getter) so the UI reflects it.\n- **`Logs`/`FunctionResult` can be silently dropped.** Both are subject to a\n title-configured byte-size ceiling; check `LogsTooLarge` /\n `FunctionResultTooLarge` before assuming absence means the script produced\n nothing. Whether `Logs` is populated at all (even under the size limit) also\n depends on a title setting — some titles never reveal script logs to\n clients.\n- **Keys in your JSON payload can't contain `.` or `$`.** This is a MongoDB\n field-name restriction the backend enforces recursively on\n `functionParameter` (and on whatever the script returns) — a payload with a\n dotted or `$`-prefixed key fails with `InvalidFieldName` before the script\n even starts. Stick to plain alphanumeric/underscore keys.\n- **Prefer a dedicated module when one exists.** Cloud Code has no typed\n contract, no cache integration, and no per-feature event — reach for it only\n when the feature genuinely isn't covered elsewhere.\n",
|
|
3
|
+
"description": "",
|
|
4
|
+
"content": "---\r\nname: cloud-code\r\ndescription: >-\r\n Write and call custom server-side game logic on the iDosGames platform:\r\n author a CloudCode handler (sandboxed JavaScript with a server.* API) and\r\n invoke it from the game via client.cloudCode (CloudCodeService) with an\r\n arbitrary JSON payload. Use this whenever the user wants bespoke server\r\n logic, a \"cloud script\", \"cloud function\" or \"server callable\", logic that\r\n must be authoritative (granting rewards, validating a reported result,\r\n anti-cheat), a write to protected player data (UserCustomData ReadOnly /\r\n Internal buckets) or to shared title state (TitleCustomData Runtime scope),\r\n or otherwise touches client.cloudCode, CloudCodeService, handlers,\r\n server.SetUserCustomData, server.IncrementTitleCustomData,\r\n server.HttpRequest or ExecuteCloudCodeResponse — even if they don't name the\r\n module explicitly. Also covers integrating a title with a third-party service\r\n (calling an external API with a stored API key, webhooks out, payment or\r\n analytics providers) and the {{secret:NAME}} / {{var:NAME}} placeholders.\r\n---\r\n\r\n# Cloud Code (iDosGames TS SDK)\r\n\r\nCloud Code runs **your** JavaScript on the platform's servers. A script defines\r\nnamed handlers; the game calls one by name and gets back whatever JSON it\r\nreturns.\r\n\r\nTwo distinct reasons to reach for it:\r\n\r\n1. **Authority.** Client code that \"grants\" a reward, \"validates\" a score or\r\n \"unlocks\" a level is a suggestion — the player owns the browser. Logic whose\r\n outcome must be trusted belongs in a handler.\r\n2. **Protected data.** The `ReadOnly`/`Internal` buckets of a player's\r\n UserCustomData and the `Runtime` scope of the title's data store have no\r\n client write path at all. A handler is the only way to write them.\r\n\r\nIf a dedicated module already covers what you need (currency, item, store,\r\nquest, character, leaderboard…), prefer that module — it gives you typed\r\nrequest/response shapes and cache integration; Cloud Code gives you neither.\r\n\r\nPublishing a script is a platform operation, not an SDK one: the AI Coder does\r\nit with its `SaveCloudCode` tool, an external agent with the backend MCP's\r\n`publish_cloud_code`, and a publisher from the dashboard. Publishing **replaces\r\nthe whole revision** — read the current source first (`GetCloudCode` with\r\n`include_code`, or `get_cloud_code`) and send it back with your handler added,\r\nor you silently delete every handler the game still calls.\r\n\r\n## Mental model\r\n\r\nThere is exactly one client-facing action: `execute`. You pass a **function\r\nname** (a handler defined in the title's deployed script's `handlers` object)\r\nand an optional **arbitrary JSON payload**; the server runs it in a sandboxed\r\nJS engine and returns an arbitrary JSON result plus execution metadata (logs,\r\ntiming, error info). The SDK has no idea what a given script's args or result\r\nlook like — **you** know your title's script contract, so you type the\r\npayload and result yourself (see Gotchas). There's no \"config vs state\" split\r\nhere like other modules — Cloud Code has no persistent per-player data model\r\nof its own; it's pure request/response.\r\n\r\nScript failure is a **first-class outcome, not a network error**: if the\r\nscript throws, times out, is rate-limited, or the handler doesn't exist, the\r\ncall still comes back `{ ok: true, data }` with `data.Error` populated and\r\n`data.FunctionResult` empty. Only infrastructure problems (not logged in, bad\r\nlocal args, connection issues, backend down) surface as `{ ok: false }`.\r\n\r\n## Writing a handler\r\n\r\nA revision is one plain JavaScript file that fills the global `handlers` object.\r\nNo imports, no modules, no `async`/`await`, no `fetch` — everything you can touch\r\nis on `server.*` and `log.*`, and every call is synchronous. Outbound network\r\naccess exists but only through `server.HttpRequest`, and only to hosts the\r\npublisher allow-listed (see _Calling another service_).\r\n\r\n```js\r\nhandlers.claimDailyBonus = function (args, context) {\r\n // context: { UserID, FunctionName, Revision, InvokedAt }\r\n var data = server.GetUserCustomData();\r\n if (!data.Success) throw new Error(data.Error);\r\n\r\n var last = data.Data.ReadOnly[\"daily_claimed_at\"];\r\n var today = new Date().toISOString().slice(0, 10);\r\n if (last && last.Value === today)\r\n return { granted: false, reason: \"already_claimed\" };\r\n\r\n var write = server.SetUserCustomData(\"ReadOnly\", \"daily_claimed_at\", today);\r\n if (!write.Success) throw new Error(write.Error);\r\n\r\n server.IncrementTitleCustomData(\"Public\", \"daily_claims_total\", 1);\r\n log.Info(\"daily bonus granted\", { user: context.UserID });\r\n return { granted: true };\r\n};\r\n```\r\n\r\n### The `server.*` API\r\n\r\nEvery call returns `{ Success, Error, Data }` — **check `Success`**; a rejected\r\nwrite (limit hit, wrong bucket, version conflict) is a normal result, not a\r\nthrow. Each call also counts against the per-execution API budget, so batch.\r\n\r\n| Call | What it does |\r\n| ----------------------------------------------------------------- | ------------------------------------------------------------------ |\r\n| `server.ReadUserData([\"InventoryV2\", \"Premium\", …])` | Read whitelisted sections of the caller's player document. |\r\n| `server.GetTitleConfig(\"Currency\", \"Item\", …)` | Read the title's configuration sections. |\r\n| `server.GetUserCustomData()` | All four buckets of the caller, **including `Internal`**. |\r\n| `server.GetPublicUserCustomDataOf(userId)` | Another player's `Public` bucket. |\r\n| `server.SetUserCustomData(bucket, key, value)` | Write any bucket — this is the protected-data write. |\r\n| `server.DeleteUserCustomData(bucket, key)` | Delete a key from any bucket (idempotent). |\r\n| `server.BatchSetUserCustomData([{Bucket, KeyID, Value}, …])` | Atomic multi-key write (all-or-nothing). |\r\n| `server.BatchDeleteUserCustomData([{Bucket, KeyID}, …])` | Atomic multi-key delete. |\r\n| `server.GetTitleCustomData()` | Title store: both scopes, both buckets. |\r\n| `server.SetTitleCustomData(bucket, key, value, expectedVersion?)` | Write the title's `Runtime` scope; pass a version for CAS. |\r\n| `server.IncrementTitleCustomData(bucket, key, delta)` | Atomic counter on shared data — use this, never read-modify-write. |\r\n| `server.DeleteTitleCustomData(bucket, key)` | Delete a `Runtime` key. |\r\n| `server.BatchSetTitleCustomData` / `BatchDeleteTitleCustomData` | Atomic multi-key variants for the title store. |\r\n| `server.GetIntegrationVariable(name)` | Read a non-secret integration setting (base URL, account id). |\r\n| `server.HttpRequest({ Method, Url, Headers, Body, ContentType })` | Call an external API — the only way out of the sandbox. |\r\n| `server.AddQuestProgress(metricID, value)` | Advance quest objectives configured with `Source: \"ServerApi\"`. |\r\n\r\n`server.AddQuestProgress` is the only way to move a `ServerApi` objective —\r\nneither the client nor the dashboard can touch those. Use it when only the server\r\nknows the fact (anti-cheat verdict, match result, an external system confirming\r\nvia `server.HttpRequest`). Unlike the client's `addQuestProgress` it neither bans\r\nnor clamps on `MaxProgressPerCall`: the script is written by the title owner, so\r\nthe value is trusted. It still clamps to the objective's `TargetValue`. Quests\r\nwhose objectives use `ClientApi` or `SystemEvent` are unreachable from here.\r\n\r\n`log.Debug/Info/Warning/Error(message, data?)` records a line the publisher sees\r\n(and, if the title reveals logs, the client too). It costs no API budget.\r\n\r\nNotes that bite:\r\n\r\n- Bucket and scope names are **case-sensitive strings**: `\"Private\"`,\r\n `\"Public\"`, `\"ReadOnly\"`, `\"Internal\"` for player data; `\"Public\"`,\r\n `\"Private\"` for title data. Anything else comes back as an error result.\r\n- Title writes always land in the `Runtime` scope — the `Static` scope is\r\n authored configuration and a script cannot touch it.\r\n- Shared counters must go through `IncrementTitleCustomData` (or\r\n `SetTitleCustomData` with `expectedVersion` from the record you read).\r\n Read-then-write from two concurrent calls silently loses one of them.\r\n- `throw` inside a handler is fine — it reaches the caller as a script-level\r\n error with your message, which is usually what you want for \"not allowed\".\r\n\r\n### Calling another service\r\n\r\nA handler can call a third-party API. The credential never appears in your code:\r\nyou reference it by placeholder and the platform substitutes it after your script\r\nhas run, immediately before the request leaves.\r\n\r\n```js\r\nhandlers.notifyDiscord = function (args, context) {\r\n var res = server.HttpRequest({\r\n Method: \"POST\",\r\n Url: \"https://discord.com/api/webhooks/{{var:DISCORD_WEBHOOK_PATH}}\",\r\n Headers: { Authorization: \"Bearer {{secret:DISCORD_TOKEN}}\" },\r\n Body: JSON.stringify({ content: \"Player \" + context.UserID + \" won!\" }),\r\n });\r\n if (!res.Success) throw new Error(res.Error); // network/policy failure\r\n if (!res.Data.Ok) return { sent: false, status: res.Data.Status };\r\n return { sent: true };\r\n};\r\n```\r\n\r\n- `{{secret:NAME}}` — an API key or token. **You can never read its value**, in\r\n any tool or any call; there is no `GetSecret`. That is deliberate: a value in\r\n JS could be returned to the player or logged by accident.\r\n- `{{var:NAME}}` — a non-secret setting. Also readable with\r\n `server.GetIntegrationVariable(name)` when you need it as a value.\r\n- `res.Data` is `{ Status, Ok, Body, BodyTooLarge, ContentType }`. `Body` is a\r\n string — parse it yourself; anything matching a substituted secret is replaced\r\n with `***` before you see it.\r\n\r\nWhat the platform enforces, and what you cannot work around from a script:\r\n\r\n- **Only allow-listed hosts.** The publisher lists them per title; there is no\r\n allow-all. An unlisted host fails with a clear message — surface it rather than\r\n retrying.\r\n- **https only** (unless the title explicitly allows plain http), **no\r\n redirects**, and no requests to private/loopback addresses.\r\n- **Per-execution request cap** (3 by default) and a **response size cap** — an\r\n oversized body is dropped, not truncated, with `BodyTooLarge: true`.\r\n- The whole call still lives inside the 10-second execution budget, so one slow\r\n integration can starve everything after it.\r\n\r\nIf the credential or the host you need does not exist yet, say exactly what has\r\nto be added in the title's **Integrations** settings — you cannot add either one.\r\n\r\n### Limits you are designing against\r\n\r\n10 seconds of wall-clock per call (hard, whatever the title configures), a cap\r\non statements and recursion depth, a cap on `server.*` calls per execution, and\r\nbyte ceilings on the returned result and the logs. Handlers are short decisions,\r\nnot jobs.\r\n\r\n## Setup\r\n\r\n```ts\r\nimport { createIDosGamesClient } from \"@idosgames/core\";\r\n\r\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\r\nawait client.auth.loginWithDeviceID(); // or any auth.* method\r\n\r\nconst cloudCode = client.cloudCode; // the CloudCodeService\r\n```\r\n\r\nRequires an authenticated session — without one, `execute` returns\r\n`{ ok: false, reason: \"unauthorized\" }` rather than making a request.\r\n\r\n## Methods\r\n\r\n`execute` returns `Promise<OperationResult<ExecuteCloudCodeResponse>>`: either\r\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\r\n`result.ok` before touching `result.data` — and then check `data.Error` before\r\ntrusting `data.FunctionResult` (see below). `reason` is one of `\"client\"`\r\n(empty/whitespace-only function name), `\"unauthorized\"`, `\"throttled\"` (fired\r\nthe same call again inside the throttle window), `\"connection\"` (transient,\r\noffer Retry), `\"validation\"` (response/schema drift), or `\"server\"`\r\n(infrastructure-level rejection — `error` carries the human-readable reason).\r\n\r\n| Method | Purpose | `data` on success |\r\n| ---------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------- |\r\n| `execute(functionName, functionParameter?, revisionSelection?, specificRevision?)` | Run a title-defined cloud script handler by name. | `ExecuteCloudCodeResponse` |\r\n\r\nParameters:\r\n\r\n- `functionName` — the handler name inside the deployed script's `handlers`\r\n object. Case-sensitive; trimmed before sending. Client-side, only\r\n empty/whitespace is rejected (`reason: \"client\"`). Server-side, the backend\r\n additionally rejects (as a script-level `InvalidFieldName` error, not an\r\n `OperationResult` failure) names containing `.`, `$`, whitespace, or control\r\n characters, or longer than 128 characters — these are illegal as MongoDB\r\n field names since the name can end up in audit/log paths.\r\n- `functionParameter?` — any `JsonValue` (object, array, string, number,\r\n boolean, or null) passed as the handler's first argument. Omit if the script\r\n needs no input. If it's an object (at any nesting depth), none of its keys\r\n may contain `.` or `$` — the backend rejects such payloads with a\r\n script-level `InvalidFieldName` error before the script ever runs.\r\n- `revisionSelection?` — `\"Live\"` (default when omitted), `\"Latest\"`, or\r\n `\"Specific\"`. Lets you target a non-live revision for testing.\r\n- `specificRevision?` — the revision number to run; only used when\r\n `revisionSelection` is `\"Specific\"`.\r\n\r\n`ExecuteCloudCodeResponse` shape:\r\n\r\n| Field | Meaning |\r\n| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |\r\n| `FunctionName` | Echo of the handler that ran. |\r\n| `Revision` | Which revision actually executed. |\r\n| `FunctionResult` | The script's return value — arbitrary JSON, `null` if it returned nothing or on error. |\r\n| `FunctionResultTooLarge` | `true` if the result was dropped for exceeding the title's result-size limit (`FunctionResult` is `null` in that case). |\r\n| `Logs` | Array of `{ Level, Message?, Data? }` entries from `log.debug/info/warn/error` calls inside the script. |\r\n| `LogsTooLarge` | `true` if logs were truncated for exceeding the title's log-size limit. |\r\n| `ExecutionTimeSeconds` | Server-side wall-clock execution duration. |\r\n| `APIRequestsIssued` | Count of server API calls the script made internally (e.g. reading user data) — counts toward a per-execution cap. |\r\n| `Error` | `{ Error: CloudCodeErrorCode, Message?, StackTrace? }`, present only when the script failed or never ran; `null`/absent on success. |\r\n\r\n`CloudCodeErrorCode` values: `None`, `Disabled`, `NoActiveRevision`,\r\n`RevisionNotFound`, `InvalidFieldName`, `RateLimited`, `HandlerNotFound`,\r\n`HandlerDisabled`, `Timeout`, `StatementCountExceeded`, `StackOverflow`,\r\n`ApiCallLimitExceeded`, `JavaScriptException`, `ExecutionError` — stable, safe\r\nto switch on for retry/UX logic (e.g. treat `RateLimited`/`Timeout` as\r\nretryable, others as not).\r\n\r\nOn success, the SDK emits an event — it does **not** write anything into\r\n`client.data`, since the result shape is script-specific and there's no\r\ngeneric cache slot for it. If your script mutates player state (grants\r\ncurrency, items, etc. via server-side APIs), re-fetch that state through its\r\nowning module afterward — Cloud Code itself won't refresh your local cache.\r\n\r\n## Events\r\n\r\nSubscribe with `client.on(...)`; returns an unsubscribe fn.\r\n\r\n- `cloudCode:executed` → `ExecuteCloudCodeResponse` — fired whenever `execute` returns `{ ok: true }`, regardless of whether the script itself succeeded (check `data.Error` inside the handler).\r\n\r\n```ts\r\nconst off = client.on(\"cloudCode:executed\", (r) => {\r\n if (r.Error) console.warn(\"script failed:\", r.Error.Error, r.Error.Message);\r\n});\r\n// later: off();\r\n```\r\n\r\n## Recipes\r\n\r\n### Call a script and handle both failure layers\r\n\r\n```ts\r\ninterface GrantBonusArgs {\r\n reason: string;\r\n}\r\ninterface GrantBonusResult {\r\n granted: number;\r\n}\r\n\r\nconst args: GrantBonusArgs = { reason: \"daily\" };\r\nconst result = await client.cloudCode.execute(\"grantLoginBonus\", args);\r\nif (!result.ok) return showError(result.error ?? result.reason); // infra-level failure\r\n\r\nif (result.data.Error) {\r\n return showError(result.data.Error.Message ?? result.data.Error.Error); // script-level failure\r\n}\r\n\r\nconst payload = result.data.FunctionResult as GrantBonusResult; // your contract — cast/validate it yourself\r\nconsole.log(`granted ${payload.granted}`);\r\n```\r\n\r\n### Fire-and-forget script with no input\r\n\r\n```ts\r\nconst result = await client.cloudCode.execute(\"resetDailyQuests\");\r\nif (!result.ok || result.data.Error) {\r\n console.warn(\"resetDailyQuests failed\", result.error ?? result.data.Error);\r\n}\r\n```\r\n\r\n### Test against a specific revision before it goes live\r\n\r\n```ts\r\nconst result = await client.cloudCode.execute(\r\n \"computeMatchReward\",\r\n { matchID },\r\n \"Specific\",\r\n 42, // revision number\r\n);\r\n```\r\n\r\n### Surface script logs during development\r\n\r\n```ts\r\nconst result = await client.cloudCode.execute(\"debugScript\", { x: 1 });\r\nif (result.ok) {\r\n for (const log of result.data.Logs ?? []) {\r\n console.log(`[${log.Level}]`, log.Message, log.Data);\r\n }\r\n}\r\n```\r\n\r\nLogs only come back at all if the title has logs enabled for clients; on\r\ntitles that don't, `Logs` is always an empty array even though the script did\r\nlog server-side — don't treat an empty array as proof the script logged\r\nnothing.\r\n\r\n### Chain a cloud-code call with a resource refresh\r\n\r\n```ts\r\nconst res = await client.cloudCode.execute(\"craftSpecialItem\", { recipeID });\r\nif (!res.ok || res.data.Error) return showError(res.error ?? res.data.Error);\r\n\r\n// The script granted items/currency server-side — Cloud Code didn't touch the\r\n// cache, so pull the owning module's state to see the new balance/inventory.\r\nawait client.user.getClientState(); // or the specific module's getter, e.g. client.item...\r\n```\r\n\r\n## Gotchas\r\n\r\n- **Two failure layers, don't conflate them.** `result.ok === false` means the\r\n call itself failed (auth, bad args, connection) — the script never ran or\r\n its outcome is unknown. `result.ok === true && result.data.Error` means the\r\n call succeeded but the _script_ failed (threw, timed out, disabled,\r\n unknown/undeclared handler, rate-limited) — always check both before\r\n trusting `FunctionResult`.\r\n- **Unknown handler is a script-level error, not a client-side check.** The\r\n SDK never validates that `functionName` refers to a real handler — that's\r\n entirely server-side. Depending on the title's config you can get\r\n `HandlerNotFound` either because the name isn't in the title's declared\r\n handler whitelist, or because the deployed script simply never defined\r\n `handlers[functionName]`; both look the same to the caller. A handler can\r\n also be individually killed by an admin, which comes back as\r\n `HandlerDisabled`.\r\n- **A hard 10-second ceiling always applies.** Whatever timeout the title/\r\n revision configures, the backend clamps every single execution to a 10\r\n second wall-clock budget; past that you get `Timeout` no matter what. Don't\r\n design a script-based feature around long-running work.\r\n- **Rate limiting can hit independently of the generic per-endpoint throttle.**\r\n Beyond the SDK's own ~600ms client-side throttle per call and the\r\n transport's per-user rate limit, the title can configure CloudCode-specific\r\n limits at three levels — whole title, this user, or this user+handler pair.\r\n Any of them tripping comes back as `data.Error.Error === \"RateLimited\"`\r\n (an in-band script-level outcome, `result.ok` is still `true`), with\r\n `data.Error.Message` naming which layer triggered it — treat it as\r\n retryable-after-a-delay, not a hard failure.\r\n- **No client-side validation of script logic.** The SDK only validates that\r\n `functionName` is non-empty and that you're logged in. Argument shape,\r\n business rules, and error handling are entirely up to the script — a\r\n malformed `functionParameter` will fail server-side (`JavaScriptException`\r\n or similar), not client-side.\r\n- **Type the payload and result yourself.** `functionParameter` is `JsonValue`\r\n and `FunctionResult` is `JsonValue | null` — the SDK has no schema for your\r\n title's specific scripts. Define your own request/response interfaces per\r\n handler (as in the recipes above) and cast/validate after the call.\r\n- **Cloud Code doesn't touch `client.data`.** Unlike feature modules, a\r\n successful `execute` doesn't mirror anything into the cache. If the script\r\n changed player-facing state, re-fetch it via the owning module (e.g. call\r\n the Economy/Item/Character module's getter) so the UI reflects it.\r\n- **`Logs`/`FunctionResult` can be silently dropped.** Both are subject to a\r\n title-configured byte-size ceiling; check `LogsTooLarge` /\r\n `FunctionResultTooLarge` before assuming absence means the script produced\r\n nothing. Whether `Logs` is populated at all (even under the size limit) also\r\n depends on a title setting — some titles never reveal script logs to\r\n clients.\r\n- **Keys in your JSON payload can't contain `.` or `$`.** This is a MongoDB\r\n field-name restriction the backend enforces recursively on\r\n `functionParameter` (and on whatever the script returns) — a payload with a\r\n dotted or `$`-prefixed key fails with `InvalidFieldName` before the script\r\n even starts. Stick to plain alphanumeric/underscore keys.\r\n- **Never put a third-party key in game code.** The project ships to the\r\n player's browser; a key there is a public key. The call belongs in a handler,\r\n and the key belongs in the title's integration store.\r\n- **Treat an integration's response as untrusted.** Check `Status`, don't echo\r\n the whole body back to the player, and never write an unvalidated field\r\n straight into player data.\r\n- **Prefer a dedicated module when one exists.** Cloud Code has no typed\r\n contract, no cache integration, and no per-feature event — reach for it only\r\n when the feature genuinely isn't covered elsewhere.\r\n- **Publishing replaces everything.** A revision is the whole script: publish\r\n one containing only your new handler and every other handler stops existing,\r\n with the game getting `HandlerNotFound` at runtime and nothing failing at\r\n build time. Always read the live source first and extend it.\r\n- **The handler whitelist is separate from the code.** A title can declare the\r\n handlers it allows; a function that exists in the script but not in that list\r\n is rejected with `HandlerNotFound`. When you add a handler to a title that\r\n uses a whitelist, add it to the list in the same publish.\r\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "idosgames-agent-debug-surface",
|
|
3
|
+
"description": "Make a module observable and controllable by the AI Coder's agent through ctx.exposeToAgent — the ModuleAgentApi contract (state, actions, describeActions) that backs the GetGameState and GameAction tools. Read it BEFORE writing any module that renders into a canvas (three.js, Phaser, Pixi, raw WebGL/2d) — publishing the surface and avoiding Pointer Lock are part of building one. Also use it whenever a rendered game has to be debugged or verified in the live preview, when the agent reports \"the DOM shows nothing about this game\", or when a developer adds player/world state or agent-drivable actions to a module.",
|
|
4
|
+
"content": "---\nname: idosgames-agent-debug-surface\ndescription: >-\n Make a module observable and controllable by the AI Coder's agent through ctx.exposeToAgent — the\n ModuleAgentApi contract (state, actions, describeActions) that backs the GetGameState and\n GameAction tools. Read it BEFORE writing any module that renders into a canvas (three.js, Phaser,\n Pixi, raw WebGL/2d) — publishing the surface and avoiding Pointer Lock are part of building one.\n Also use it whenever a rendered game has to be debugged or verified in the live preview, when the\n agent reports \"the DOM shows nothing about this game\", or when a developer adds player/world\n state or agent-drivable actions to a module.\n---\n\n# Making a module visible to the agent (`ctx.exposeToAgent`)\n\nThe agent inspects a running app by reading its DOM. A rendered game has no DOM to read — it is one\n`<canvas>` — so what the game _is doing_ is invisible unless the module says so itself.\n\n`ctx.exposeToAgent` closes that gap. The module publishes a small debug surface; the preview probe\npicks it up and the agent reaches it with two tools:\n\n- **GetGameState** — reads `state()` of every module that opted in, plus the list of its actions.\n- **GameAction** — calls one action and returns the state after it.\n\n## What the agent already sees without you\n\n`GetGameState` always returns an **automatic layer** first, measured by the preview probe with no\ncooperation from the game: which canvas and graphics context exist, fps, frames and draw calls,\na sparse grid of pixels read back from the frame, and the host's own state (login screen vs game).\n\nWhere the engine can be identified, its own numbers come too:\n\n| Engine | How it is found | What you get |\n| -------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |\n| three.js | `__THREE_DEVTOOLS__` — three announces itself | renderer info (draw calls, triangles, textures), scene object/mesh/light counts, camera position + look direction |\n| PixiJS 8 | `__PIXI_APP_INIT__` / `__PIXI_RENDERER_INIT__` — Pixi announces itself | stage display objects, hidden count, tree depth, ticker fps, renderer backend and resolution |\n| Phaser | looked up in the page globals — Phaser has no such channel in its release build | scenes with active/visible flags, per-scene object counts, main camera scroll and zoom, loop fps |\n\n**Phaser needs one line from you.** Phaser assigns `window.PHASER_GAME` itself, but only in its\ndebug build — the release build shipped by npm has that branch stripped, so a Phaser game is\ninvisible to any observer until it hands itself over. Verified live: without the line the preview\nreports no engine at all; with it, scenes, object counts, camera and loop fps all come through.\n\n```ts\nthis.game = new Phaser.Game({/* … */});\n// Makes the game observable in the live preview, which reads this exact global.\n(globalThis as unknown as Record<string, unknown>).PHASER_GAME = this.game;\n```\n\nDelete it again in `destroy()` (`if (globals.PHASER_GAME === this.game) delete globals.PHASER_GAME`)\n— the Mode Router destroys a suspended mode, and a stale reference would show a game that no longer\nexists. `modules/idle-rpg` does exactly this; copy it.\n\nNote that the engine layer never replaces the surface below: it reports _scenes and objects_, never\n_what the game means_ — which hero is selected, what the score is, whose turn it is.\n\nThat layer answers \"does it render at all\" — a dead loop, a blank one-colour frame, a player stuck\non the login screen. It cannot answer anything about _gameplay_: where the player is, what the score\nis, why the character fell through the floor. That is what the surface below is for, and why a\ncanvas module is not finished without it.\n\nThe same goes for driving the game. `SendInput` dispatches synthetic keys/clicks and works for\ngames that read plain DOM events, but an engine that gates input on Pointer Lock ignores it —\nPointer Lock is unavailable inside the preview's cross-origin iframe. **Do not gate controls on\nPointer Lock**: support a soft-lock fallback (click the canvas to take control, Escape to release,\nclamp mouse deltas), or the preview is unplayable for the human too. Actions published here always\nwork, because they go through the module's own input path.\n\n## Adding the surface\n\n```ts\nsetup(ctx) {\n const { scene, agent } = createMyGame();\n ctx.registerScene(scene);\n ctx.exposeToAgent(agent);\n}\n```\n\n```ts\nimport type { ModuleAgentApi } from \"@idosgames/module-sdk\";\n\nexport function createMyGameAgentApi(\n getGame: () => Game | null,\n): ModuleAgentApi {\n return {\n state() {\n const game = getGame();\n if (!game) return { mounted: false };\n return {\n mounted: true,\n playing: game.running, // на паузе персонаж не двигается — это не баг\n player: { pos: game.player.pos, health: game.player.health },\n world: { loadedChunks: game.world.chunks.size },\n };\n },\n actions: {\n move: async (args) => hold(dirKey(args?.dir), clampMs(args?.ms)),\n jump: async () => hold(\"Space\", 120),\n },\n describeActions: {\n move: \"Walk: { dir: forward|back|left|right, ms?: number }.\",\n jump: \"Jump.\",\n },\n };\n}\n```\n\nPass a **getter**, not the game object: the scene creates the game in `mount()` and drops it in\n`destroy()`, while the surface is registered once in `setup()`.\n\n## Rules that matter\n\n1. **Actions go through the module's real input path** — intents, command queue, the same key set\n the player's keyboard fills. A second movement implementation drifts from the real one, and then\n the agent verifies a game nobody plays.\n2. **`state()` must be cheap, JSON-serializable and free of secrets.** It is called per request and\n pasted verbatim into an LLM prompt.\n3. **Bound every action.** Clamp durations (a few seconds at most) so the agent cannot \"hold W\" for\n a minute; reject unknown argument values with a clear error naming the valid ones.\n4. **Nothing destructive.** Deleting a save, spending real currency or resetting progress does not\n belong here — this surface exists to observe and reproduce, not to administer.\n5. **`describeActions` is the only documentation the agent gets.** One line per action: what it does\n and which arguments it takes, with units.\n6. **Include the \"am I even running\" flags** (`mounted`, `playing`). Most \"the character does not\n move\" reports are a paused game, and without those flags the agent goes looking for a physics bug.\n\n## When the module's UI is DOM\n\nA module whose interface is React/DOM (panels, HUD, buttons) needs only a thin surface — the agent\nalready reads that tree and clicks it like a human. Expose what the canvas hides (scene state) and\nleave `actions` empty rather than mirroring buttons the agent can already press.\n\n## Verifying it works\n\nIn the preview, ask the agent something it can only answer by looking: \"where is the player right\nnow?\", \"walk forward for a second and tell me if the position changed\", \"jump and check that the\nplayer lands\". If `GetGameState` answers with the automatic layer but an empty `exposedByGame`, the\nsurface is not wired — check that `exposeToAgent` runs inside `setup()` and that the host is\n`@idosgames/app-shell` (the bridge publishes the registry).\n",
|
|
5
|
+
"references": []
|
|
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 host shell and plug in feature modules (board-game, idle-rpg, voxelcraft, …). 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). 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 host shell\n and plug in feature modules (board-game, idle-rpg, voxelcraft, …). 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). 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```\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[] = [ … ]\nsrc/modules/{id}/ # each module's source (from get_module)\n```\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. **Host** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/host-starter`). Its `src/modules.ts` starts empty → the host shows a \"no modules\" state.\n2. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`.\
|
|
4
|
+
"content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the host shell\n and plug in feature modules (board-game, idle-rpg, voxelcraft, …). 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). 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```\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[] = [ … ]\nsrc/modules/{id}/ # each module's source (from get_module)\n```\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. **Host** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/host-starter`). Its `src/modules.ts` starts empty → the host shows a \"no modules\" state.\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}/`.\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`).\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** — the host renders a nav-bar when ≥2 modules register a route, and mode-switches between\n them; a shared HUD panel (`activeOnly:false`) stays visible across all modes.\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`, authenticated with an `X-MCP-API-Key` header (the\n publisher issues the key per Title on platform.idosgames.com); every tool call takes a `title_id`\n argument. Connect it as an HTTP MCP server — and keep the key out of committed config via env\n expansion:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\",\n \"headers\": { \"X-MCP-API-Key\": \"${IDOS_MCP_API_KEY}\" }\n }\n }\n }\n ```\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
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), or asks about defineModule, registerScene/registerPanel/registerRoute, activate/suspend, SceneMountContext, 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), or asks about defineModule, registerScene/registerPanel/registerRoute,\n activate/suspend, SceneMountContext, 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 (cross-module bus) · 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: \"🎲\" }); // nav/mode entry\n },\n});\n```\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\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\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## 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",
|
|
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), or asks about defineModule, registerScene/registerPanel/registerRoute,\n activate/suspend, SceneMountContext, 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 (cross-module bus) · 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: \"🎲\" }); // nav/mode entry\n },\n});\n```\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## 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\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## 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
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "idosgames-title-bootstrap",
|
|
3
|
+
"description": "Configure a FRESH (empty) iDosGames Title so a game can actually run against it: currencies with starting balances, then the game-loop board config, then verify with a real login. Use this when a newly created Title answers \"Board not found\", \"Board not enabled\", \"Bots config is not configured\", or \"SpecialMode ... must be configured\", when starting balances don't appear, or whenever you scaffold a project for a Title that was just created and has no config yet. All writes go through the Title-configuration MCP (see idosgames-getting-started for how to connect it).",
|
|
4
|
+
"content": "---\nname: idosgames-title-bootstrap\ndescription: >-\n Configure a FRESH (empty) iDosGames Title so a game can actually run against it: currencies with\n starting balances, then the game-loop board config, then verify with a real login. Use this when a\n newly created Title answers \"Board not found\", \"Board not enabled\", \"Bots config is not\n configured\", or \"SpecialMode ... must be configured\", when starting balances don't appear, or\n whenever you scaffold a project for a Title that was just created and has no config yet. All\n writes go through the Title-configuration MCP (see idosgames-getting-started for how to connect\n it).\n---\n\n# Bootstrap an empty Title\n\nA freshly created Title has an **empty `TitlePublicConfiguration`** — the game client will log in\nfine, but every feature that reads config fails until its section exists. Configure it over the\nTitle-configuration MCP (`POST https://site.idosgames.com/api/v2/mcp`, `X-MCP-API-Key` header,\nevery tool takes `title_id`). Tools are `get_<section>` / `save_<section>` — snake_case of the\nconfig model's property names (`Currency` → `save_currency`, `GameLoop` → `save_game_loop`).\n\n**Always `get_` a section before `save_` — save replaces the whole section**, so build on what is\nthere rather than authoring blind.\n\n## Error → missing config\n\n| Server error | What's missing |\n| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |\n| `Board not found` / `Board not enabled` | `GameLoop.Board` — the whole board definition |\n| `Stage not found` / `BoardTemplate not found for stage` / `StageTemplate not found for stage` | `StagesByLevel[\"1\"]` or the template it references by id |\n| `Bots config is not configured (Bots.RankMultiplierMin/Max ...)` | `Board.Bots` — required as soon as any tile can trigger Attack/Raid |\n| `SpecialMode '<id>' OfferExpireSeconds must be configured (> 0)` (same for `ClaimExpireSeconds`) | that mode in `Board.SpecialModesByID` |\n| Player starts with zero of everything | `Currency` entries' `InitialDeposit` |\n\n## Order of operations\n\n### 1. Currencies (`save_currency`)\n\nDefine every currency the game references **before** the game loop that spends them. For the\nboard-game module that is three roles: a roll currency (dice), a shield currency, and a soft\ncurrency (building costs / rewards). Give each an `InitialDeposit` for the starting balance.\n\n`InitialDeposit` applies when a **user is created** — an account that logged in before the deposit\nwas configured stays at 0. When verifying, log in as a **fresh guest**, don't reuse the session.\n\n### 2. Game loop (`save_game_loop`)\n\nThe `Board` object wires everything together. Minimum viable shape:\n\n- `RollCurrencyID` / `ShieldCurrencyID` / `SoftCurrencyID` — ids from step 1.\n- `BoardTemplatesByID` — at least one template with the tile ring (`Reward`, `Chance`, `Attack`,\n `Raid`, `Special`, `Shield`, `Empty`, `RandomAction`).\n- `StageTemplatesByID` — at least one economy template (`StageOperations`: `OnBuild`,\n `OnTileLanding`, `OnStageComplete`, `SpecialModesByID`, …).\n- `StagesByLevel` — `{\"1\": {...}}` referencing a `BoardTemplateID` + `StageTemplateID` that exist\n in the two maps above (dangling ids are a runtime error, not a save error).\n- `AllowedRollMultipliers`, `Dice`.\n- `Bots` — **required** if any tile can resolve to Attack or Raid: `RankMultiplierMin`/`Max` with\n `Max >= Min > 0`.\n- `RaidMode` — `Sequential` (server reveals cell by cell) or `Fast` (client reveals locally from\n the pre-dealt layout, submits once). Pick one; the client adapts.\n- Any `SpecialModesByID` mode needs `OfferExpireSeconds > 0` and `ClaimExpireSeconds > 0`.\n\n### 3. Verify against the live backend\n\n1. Fresh guest login → starting balances match the `InitialDeposit`s.\n2. `client.gameLoop.getUserBoardState()` → no `Board not enabled`.\n3. Roll until each tile type triggers once — Reward, Chance, Attack, Raid, Special — and confirm\n the granted/spent currencies match the configured economy.\n\n## Scope\n\nThis checklist covers the board-game loop because it is the config-heaviest module. Other config\nsections (store, quests, lootboxes, …) follow the same pattern — `get_<section>`, fill, `save_`,\nverify with the matching `@idosgames/core` service — and each service's own skill documents the\nshape it reads.\n",
|
|
5
|
+
"references": []
|
|
6
|
+
}
|