@idosgames/mcp 0.1.22 → 0.1.23
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/package.json +1 -1
- package/registry/host.json +2 -2
- package/registry/index.json +122 -122
- package/registry/modules/ai.json +5 -5
- package/registry/modules/board-game.json +9 -9
- package/registry/modules/boosts.json +7 -7
- package/registry/modules/character.json +6 -6
- package/registry/modules/chat.json +3 -3
- package/registry/modules/collections.json +5 -5
- package/registry/modules/coop-events.json +5 -5
- package/registry/modules/craft.json +5 -5
- package/registry/modules/deals.json +5 -5
- package/registry/modules/events.json +5 -5
- package/registry/modules/friends.json +5 -5
- package/registry/modules/idle-rpg.json +8 -8
- package/registry/modules/inventory.json +5 -5
- package/registry/modules/invite.json +5 -5
- package/registry/modules/leaderboard.json +5 -5
- package/registry/modules/lootboxes.json +6 -6
- package/registry/modules/mailbox.json +5 -5
- package/registry/modules/marketplace.json +6 -6
- package/registry/modules/mini-game.json +3 -3
- package/registry/modules/multiplayer.json +7 -7
- package/registry/modules/premium.json +5 -5
- package/registry/modules/profile.json +3 -3
- package/registry/modules/pvp.json +7 -7
- package/registry/modules/quests.json +5 -5
- package/registry/modules/rewards.json +5 -5
- package/registry/modules/season.json +5 -5
- package/registry/modules/store.json +5 -5
- package/registry/modules/tutorials.json +3 -3
- package/registry/modules/voxelcraft.json +6 -6
- package/registry/modules/workshop.json +3 -3
- package/registry/skills/blockchain-system.json +1 -1
- package/registry/skills/community-marketing-system.json +1 -1
- package/registry/skills/idosgames-getting-started.json +1 -1
- package/registry/skills/idosgames-ui-kit.json +1 -1
- package/registry/skills/mailbox-system.json +1 -1
- package/registry/skills/match-system.json +2 -2
- package/registry/skills/multiplayer-realtime.json +2 -2
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"version": "0.1.2"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.
|
|
43
|
-
"@idosgames/module-sdk": "0.8.
|
|
44
|
-
"@idosgames/react": "0.4.
|
|
42
|
+
"@idosgames/core": "0.21.0",
|
|
43
|
+
"@idosgames/module-sdk": "0.8.2",
|
|
44
|
+
"@idosgames/react": "0.4.3",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
47
|
"contentHash": "25a1e02d58fb773f2c26574d6985ffca501001350497cac8f8a067774914e490",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"references": [
|
|
6
6
|
{
|
|
7
7
|
"path": "data-model.md",
|
|
8
|
-
"content": "# Blockchain data model — reference\n\nFull shape of the config (`BlockchainDefinitions`), player state\n(`UserBlockchainState`), transaction documents, and the withdrawal signature\npayloads. All of these are **strictly typed in the SDK** — every type below is\nexported from `@idosgames/core`, built with `zod` schemas that keep\n`.passthrough()`, so a field the backend adds later still round-trips. Field\nnames are PascalCase (straight from the backend JSON). Decimal-valued fields\n(`Amount`, balances, USD values) are decimal strings, not JS numbers — use\n`decimal.js` (already a dependency) rather than float math.\n\n## Contents\n\n- [Config: BlockchainDefinitions](#config-blockchaindefinitions) — what `getDefinitions()` returns\n- [BlockchainNetworkDefinition](#blockchainnetworkdefinition)\n- [NFT collection bindings](#nft-collection-bindings)\n- [Account safety policy](#account-safety-policy)\n- [Withdrawal gate mechanics](#withdrawal-gate-mechanics) — every check + formula the backend runs before paying out\n- [Player state: UserBlockchainState](#player-state-userblockchainstate) — what `getUserState()` returns\n- [KYC state](#kyc-state)\n- [Compliance counters](#compliance-counters) — the daily/monthly spend windows behind the limit errors\n- [Stats containers](#stats-containers)\n- [Transaction documents](#transaction-documents)\n- [Withdrawal signature payloads](#withdrawal-signature-payloads)\n- [Domain delta: BlockchainStateDelta](#domain-delta-blockchainstatedelta)\n- [Responses](#responses)\n- [Enums](#enums)\n\n---\n\n## Config: BlockchainDefinitions\n\nReturned by `getDefinitions()` as part of `BlockchainConfigResponse`; the\n`Blockchain` section is cached via\n`client.data.config.getSection<BlockchainDefinitions>(\"Blockchain\")`. The\nsibling `CryptoCurrencies` map (`Record<string, CryptoCurrencyDefinition>`)\nrides along in the same response — see the currency-system skill for that\nshape.\n\n```ts\ninterface BlockchainDefinitions {\n SystemState?: BlockchainSystemState; // title-wide kill switches\n Networks?: Record<string, BlockchainNetworkDefinition>; // key = NetworkID\n AccountSafety?: BlockchainAccountSafetyPolicy;\n}\n\ninterface BlockchainSystemState {\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n NftDepositsEnabled?: boolean;\n NftWithdrawalsEnabled?: boolean;\n PlatformOverrides?: PlatformBlockchainState; // per-platform (Ios/Android/Web) override\n}\n\ninterface PlatformBlockchainState {\n Ios?: boolean;\n Android?: boolean;\n Web?: boolean;\n}\n```\n\n`SystemState` is the title-wide switch; each `BlockchainNetworkDefinition` has\nits own matching flags that layer on top (both must allow an action for it to\nbe permitted — the backend enforces this, but mirror the check in UI to avoid\nshowing a dead button).\n\n---\n\n## BlockchainNetworkDefinition\n\nOne connected chain. Key in `Networks` is the `NetworkID` you pass to every\nservice method (`\"polygon\"`, `\"ethereum\"`, `\"solana\"`, etc. — title-defined\nstrings, not fixed by the SDK).\n\n```ts\ninterface BlockchainNetworkDefinition {\n NetworkID?: string;\n DisplayName?: string;\n Type?: \"EVM\" | \"Solana\"; // controls which signature payload shape you get back\n ChainID?: number; // EVM chain id; 0 for Solana (unused)\n ChainTicker?: string; // e.g. \"MATIC\", \"ETH\", \"SOL\" — used server-side to route RPC calls\n RewardPoolAddress?: string; // EVM: pool contract address; Solana: platform Program ID\n VaultDepositAddress?: string; // Solana-only: vault address for SPL deposits, when used\n ChainConfigVersion?: number; // default 1; controls the withdrawal signature payload format\n RequiredConfirmations?: number; // on-chain confirmations before the backend accepts a deposit; default 12\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n NftDepositsEnabled?: boolean;\n NftWithdrawalsEnabled?: boolean;\n PlatformOverrides?: PlatformBlockchainState;\n NftCollections?: BlockchainNftCollectionBinding[];\n AssetPaths?: Record<string, string>; // icons, chain logos, etc.\n}\n```\n\n`Type` determines which field is populated on withdrawal responses:\n`EvmSignature` for `\"EVM\"` networks, `SolanaSignature` for `\"Solana\"`\nnetworks. Always check `Type` (or just check which signature field is\nnon-null) rather than assuming one shape.\n\n`RequiredConfirmations` is why a `depositToken`/`depositNFT` call can fail\nright after the player submits their on-chain transaction — the backend\nwon't accept it until it has enough confirmations, returning `\"Not enough\nconfirmations (required {RequiredConfirmations}). Try again in a few\nminutes.\"` Ignored for Solana networks (finality is checked via commitment\nlevel instead). Surface \"still confirming, try again shortly\" for that\nspecific message rather than a hard failure.\n\n---\n\n## NFT collection bindings\n\nBinds one on-chain NFT contract/collection to an in-game item catalog, so the\nbackend knows which `ItemCatalogID` a deposited/withdrawn NFT maps to.\n\n```ts\ninterface BlockchainNftCollectionBinding {\n ContractAddress?: string;\n ItemCatalogID?: string; // which item catalog this contract maps to in-game\n DisplayName?: string;\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n}\n```\n\nA network can bind multiple collections (e.g. one contract for weapon NFTs,\nanother for cosmetic NFTs), each independently toggle-able.\n\n---\n\n## Account safety policy\n\n```ts\ninterface BlockchainAccountSafetyPolicy {\n MinAccountAgeDays?: number; // account must be at least this old to withdraw; default 7\n MultiAccountCheckEnabled?: boolean; // default true\n BanOnSharedWithdrawalAddress?: boolean; // default true; see below\n PendingWithdrawalTtlHours?: number; // signature validity window; default 24, min enforced 1\n}\n```\n\nRead-only/informational for the client — the backend enforces these; there's\nnothing to compute. Useful for showing a \"why is withdrawal locked\" message\n(e.g. \"Available after your account is 7 days old\"). Checked only on\nwithdrawal requests and `retryWithdrawal` — deposits are always accepted\nregardless of account age (an account can be auto-flagged from a deposit, but\nnever blocked from making one).\n\n`MultiAccountCheckEnabled` + `BanOnSharedWithdrawalAddress` together mean: if\na player requests a withdrawal to a wallet address that was already used as a\nwithdrawal _or deposit_ destination by a **different** account on this title,\nthe requesting account is **banned immediately** as part of the check (not\njust rejected) — `\"Account banned. Contact support.\"` There's no warning\nstep; a title enabling this should surface it clearly in withdrawal UI\ncopy before the player submits an address.\n\n---\n\n## Withdrawal gate mechanics\n\nThe full ordered set of server-side checks a `requestTokenWithdrawal` /\n`requestNFTWithdrawal` call goes through, with the exact backend formulas.\nThe SKILL.md's [Withdrawal gates](../SKILL.md#withdrawal-gates-what-can-reject-a-request)\nsection lists the corresponding verbatim error strings; this section is the\n\"why\" behind each one.\n\n1. **Global + per-network + per-currency + per-binding enable flags** — all\n of `BlockchainSystemState.WithdrawalsEnabled`,\n `BlockchainNetworkDefinition.WithdrawalsEnabled`,\n `CryptoCurrencyPermissions.WithdrawalsEnabled` (title-wide, all networks),\n and `CryptoNetworkBinding.WithdrawalsEnabled` (this specific\n currency+network pair) must be `true`. Any one `false` rejects the\n request — a title can pause withdrawals for one currency on one network\n (e.g. a drained hot wallet) without touching the others.\n2. **`MinWithdraw`** (`CryptoNetworkBinding.MinWithdraw`, per currency+network)\n — the requested `amount` must be `>= MinWithdraw`. There is no per-withdrawal\n fee field: what actually reduces the payout is the burn share and the pool\n commission — see the commission note below.\n3. **Balance check** — the player's `InventoryV2.CryptoCurrencies[currencyID]\n.Amount` (tokens) or owned item count (NFTs) must cover the requested\n amount.\n4. **Account safety** — see [above](#account-safety-policy).\n5. **Compliance / KYC** (`CryptoCurrencyDefinition.Limits`, all in\n USD-equivalent of the request, computed as `amountNative * ValueInUSD`):\n - `KycRequiredAboveUsd`: if the request's USD value exceeds this and\n `UserBlockchainState.Kyc.Status !== \"Verified\"`, rejected.\n - `DailyWithdrawUsd`: rejected if `DailyWithdrawnUsd (so far today) +\nthisRequestUsd > DailyWithdrawUsd`. The daily window is a fixed UTC\n calendar day (00:00 UTC), not a rolling 24h window.\n - `MonthlyWithdrawUsd`: same shape, UTC calendar month (00:00 UTC on the\n 1st).\n - Any of the three fields being absent/null on the currency disables that\n specific check.\n6. **Title-wide collective pool cap** — independent of the individual\n player's limits: the title has one `UsersWithdrawable` pool balance per\n (network, currency), fed by the remainder of every deposit after both the\n developer share (`CryptoCurrencyDefinition.DeveloperDepositSharePercent`)\n and the Community Marketing share\n (`CryptoCurrencyDefinition.CommunityMarketingDepositSharePercent`) are\n taken off the top (developer share wins on overflow if the two sum above\n 100%), plus any `donateToUsersPool` donations. A withdrawal request is\n rejected outright if `UsersWithdrawable < requestedAmount` for that pool —\n this is a platform economics limit, not a per-player one, and isn't\n exposed through any client-readable field; you only learn about it from\n the rejection.\n7. **Platform commission + EVM burn** — an operator-wide withdrawal\n commission percentage (0–100, not exposed in `BlockchainDefinitions`) is\n applied to the _gross_ requested amount, and (EVM only) a per-currency\n burn percentage (`CryptoCurrencyDefinition.WithdrawalBurnPercent`) is\n applied on top: `commission = amountNative * (commissionPercent / 100)`,\n `burn = amountNative * (WithdrawalBurnPercent / 100)` (0 on Solana),\n `net = amountNative - commission - burn`. The player is debited the full\n `amountNative` (gross); the signed payload authorizes paying out only\n `net` on-chain, with `burn` sent to the DEAD address by the contract\n itself. If `net <= 0` (commission + burn consume the whole request), the\n withdrawal is rejected before any signature is issued. This is why\n `TokenWithdrawalResponse.NetAmountNative` can be less than `AmountNative`\n — always display `NetAmountNative` as \"you'll receive,\" and\n `BurnAmountNative` if you want to show the burned portion separately. NFT\n withdrawals have no commission/burn step (no `NetAmountNative` /\n `BurnAmountNative` on `NFTWithdrawalResponse`).\n\nNone of steps 5–7 are visible ahead of time as a single client-readable\n\"can withdraw\" flag — the pattern is: attempt the call, branch on the error\nstring.\n\n---\n\n## Player state: UserBlockchainState\n\nReturned by `getUserState()` as `{ State, CryptoBalances }`\n(`UserBlockchainStateResponse`); `State` is cached at\n`client.data.user.state?.Blockchain`, `CryptoBalances` is folded into\n`client.data.user.state?.InventoryV2?.CryptoCurrencies` (same cache\n`client.data.user.getCryptoCurrencyAmount(id)` reads).\n\n```ts\ninterface UserBlockchainState {\n Version?: number;\n Stats?: BlockchainStats;\n LinkedWallets?: Record<string, LinkedWalletInfo>; // key = NetworkID\n PendingWithdrawals?: PendingWithdrawalRef[];\n Kyc?: UserKycState;\n FirstActivityAt?: string; // ISO datetime\n LastActivityAt?: string;\n IsFlagged?: boolean; // account-safety flag (see BlockchainAccountSafetyPolicy)\n FlagReason?: string;\n}\n\ninterface LinkedWalletInfo {\n NetworkID?: string;\n Address?: string;\n LinkedAt?: string;\n LastUsedAt?: string;\n LinkType?:\n \"AutoLinkedFromTransaction\" | \"SignatureVerified\" | \"ManuallyLinked\";\n IsSignatureVerified?: boolean;\n}\n\n/** Light reference only — full transaction data lives in the tx history documents. */\ninterface PendingWithdrawalRef {\n TitleTransactionID?: string;\n Type?: \"Token\" | \"Nft\";\n NetworkID?: string;\n AssetID?: string; // CurrencyID for Token withdrawals, ItemID for NFT withdrawals\n Amount?: string; // decimal string\n CreatedAt?: string;\n ExpiresAt?: string;\n}\n```\n\n`LinkedWallets` is populated automatically the first time a wallet address is\nused in a deposit/withdrawal on a network (`AutoLinkedFromTransaction`) —\nthere's no separate \"link wallet\" call in this module.\n`PendingWithdrawals` is a **light** list (id/type/asset/amount/expiry only)\nfor quickly rendering \"you have N pending withdrawals\" — cross-reference\n`TitleTransactionID` against `getTransactionHistory()` for full details\n(status, hash, fail reason).\n\n---\n\n## KYC state\n\n```ts\ninterface UserKycState {\n Status?: \"NotRequested\" | \"Pending\" | \"Verified\" | \"Rejected\" | \"Expired\";\n Tier?: \"None\" | \"Tier1\" | \"Tier2\" | \"Tier3\";\n VerifiedAt?: string;\n ExpiresAt?: string;\n RejectedAt?: string;\n ProviderReference?: string; // third-party KYC provider's reference id\n RejectionReason?: string;\n}\n```\n\nThis module surfaces KYC status for gating UI (e.g. \"verify your identity to\nwithdraw over $X\") — there's no `startKyc`/`submitKyc` method here; KYC\nverification itself happens through whatever provider integration the title\nuses outside this SDK, and this state just reflects the result.\n\n---\n\n## Compliance counters\n\nPer-currency AML spend windows that back the `\"Daily withdraw limit\nexceeded\"` / `\"Monthly withdraw limit exceeded\"` errors (see\n[Withdrawal gate mechanics](#withdrawal-gate-mechanics)). Not part of\n`UserBlockchainState` — these live alongside the balance, on each entry of\n`CryptoBalances` (the sibling map returned by `getUserState()`, cached into\n`InventoryV2.CryptoCurrencies`, read via `client.data.user\n.getCryptoCurrencyAmount(currencyID)` for the balance itself):\n\n```ts\ninterface UserCryptoComplianceCounters {\n DailyPeriodStartUtc?: string; // start of the current UTC calendar day counted\n DailyWithdrawnUsd?: string; // decimal string — USD spent so far this UTC day\n MonthlyPeriodStartUtc?: string; // start of the current UTC calendar month counted\n MonthlyWithdrawnUsd?: string; // decimal string — USD spent so far this UTC month\n}\n\ninterface UserCryptoCurrencyState {\n Amount: string; // decimal string — available balance\n Frozen: string; // decimal string — reserved by pending withdrawals\n Compliance?: UserCryptoComplianceCounters; // absent if the currency has no configured limits\n CreatedAt?: string;\n UpdatedAt?: string;\n}\n```\n\nThere is no client method to read \"USD spent so far today\" proactively — the\ncounters are internal bookkeeping the backend checks at request time and\nrolls forward automatically once the UTC day/month boundary passes (an\nexpired window resets to the new request's amount, it does not carry over).\nTreat a `\"Daily/Monthly withdraw limit exceeded (...)\"` error message as the\nonly place this data surfaces to the client, and parse the numbers out of the\nerror string if you need to show a friendlier message.\n\n---\n\n## Stats containers\n\n```ts\ninterface BlockchainStats {\n Tokens?: TokenStatsContainer;\n Nfts?: NftStatsContainer;\n}\n\ninterface TokenStatsContainer {\n TotalDeposits?: number;\n TotalWithdrawals?: number;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n TotalDepositsVolumeUsd?: string;\n TotalWithdrawalsVolumeUsd?: string;\n PerCurrency?: Record<string, TokenCurrencyStats>; // key = CurrencyID\n}\n\ninterface TokenCurrencyStats {\n CurrencyID?: string;\n Deposits?: number;\n DepositsVolumeNative?: string;\n DepositsVolumeUsd?: string;\n Withdrawals?: number;\n WithdrawalsVolumeNative?: string;\n WithdrawalsVolumeUsd?: string;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n FirstDepositAt?: string;\n LastDepositAt?: string;\n FirstWithdrawalAt?: string;\n LastWithdrawalAt?: string;\n}\n\ninterface NftStatsContainer {\n TotalDeposits?: number;\n TotalWithdrawals?: number;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n PerCollection?: Record<string, NftCollectionStats>; // key = ItemCatalogID (or composite id)\n}\n\ninterface NftCollectionStats {\n NetworkID?: string;\n ItemCatalogID?: string;\n Deposits?: number;\n Withdrawals?: number;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n FirstDepositAt?: string;\n LastDepositAt?: string;\n FirstWithdrawalAt?: string;\n LastWithdrawalAt?: string;\n}\n```\n\nLifetime counters/volumes for a player's own activity — handy for a \"your\non-chain activity\" summary screen. Purely informational; nothing to act on.\n\n---\n\n## Transaction documents\n\nReturned by `getTransactionHistory()` as `{ TokenTransactions, NFTTransactions }`\n(`TransactionHistoryResponse`). These are the full records — richer than the\nlight `PendingWithdrawalRef`. Both arrays are capped to the same `limit`\n(default 50, hard server-side ceiling 200 — values above 200 are silently\nclamped, values `<= 0` fall back to the default of 50); there's no separate\nper-type limit or pagination cursor.\n\n```ts\ninterface TokenTransactionDocument {\n ID?: string;\n TitleID?: string;\n CreatedAt?: string;\n UpdatedAt?: string;\n UserID?: string;\n TransactionHash?: string; // on-chain hash once known\n Nonce?: string;\n NetworkID?: string;\n ChainType?: string;\n ChainID?: number;\n Direction?: \"UsersCryptoWallet\" | \"Game\"; // which way the asset moved\n From?: string;\n To?: string;\n Amount?: string;\n Status?: \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\n SignatureData?: string;\n CompletedAt?: string;\n ExpiresAt?: string;\n FailReason?: string;\n Reason?: string;\n Category?: string; // operation kind (\"game_topup\", \"community_reward\", …); V2-only, absent on V1 txs\n TokenID?: string;\n CurrencyID?: string;\n AmountUsd?: string;\n NetPayoutAmount?: string; // withdrawals only: amount after platform commission\n}\n\ninterface NFTTransactionDocument {\n ID?: string;\n TitleID?: string;\n CreatedAt?: string;\n UpdatedAt?: string;\n UserID?: string;\n TransactionHash?: string;\n Nonce?: string;\n NetworkID?: string;\n ChainType?: string;\n ChainID?: number;\n Direction?: \"UsersCryptoWallet\" | \"Game\";\n From?: string;\n To?: string;\n Amount?: string;\n Status?: \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\n SignatureData?: string;\n CompletedAt?: string;\n ExpiresAt?: string;\n FailReason?: string;\n Reason?: string;\n Category?: string; // operation kind (\"game_topup\", \"community_reward\", …); V2-only, absent on V1 txs\n NFTID?: string;\n ItemID?: string;\n CatalogID?: string;\n SkinID?: string;\n}\n```\n\n`Direction: \"UsersCryptoWallet\"` = a withdrawal (asset moving to the player's\nwallet); `Direction: \"Game\"` = a deposit (asset moving into the game). `Status`\nis the authoritative lifecycle value for a transaction — cross-reference it\nagainst `PendingWithdrawalRef` (state) or the response you got from\n`requestTokenWithdrawal`/`requestNFTWithdrawal` (by `TitleTransactionID` ==\n`ID`) to know exactly where a withdrawal is:\n\n| Status | Meaning |\n| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Pending` | Requested/signed but not yet confirmed on-chain. |\n| `Completed` | Confirmed on-chain (`confirmWithdrawal` succeeded and chain verified). |\n| `Failed` | Rejected — see `FailReason`. |\n| `Expired` | Modeled in the enum but not currently assigned by this backend path. |\n| `Abandoned` | TTL (`ExpiresAt`) passed without submission — this is the status a lazily-expired pending withdrawal actually lands on, not `Expired`. Can still be closed out by a late `confirmWithdrawal` if the player submitted on-chain before the backend swept it (see the SKILL.md gotcha). |\n\nNote the divergence from what the field name suggests: **`retryWithdrawal`\nonly accepts a transaction currently in `Pending`** — it rejects\n`Failed`/`Abandoned`/`Completed` alike with `\"Transaction is not in Pending\nstate (current: {status}).\"` (see the SKILL.md's\n[Gotchas](../SKILL.md#gotchas) section for the full retry/confirm lifecycle).\nIn practice a TTL-expired withdrawal (now `Abandoned`) is **not** retryable\nthrough `retryWithdrawal` — the only path forward for one is\n`confirmWithdrawal` with a hash, if the player actually submitted the\noriginal signature before it was swept.\n\n---\n\n## Withdrawal signature payloads\n\nExactly one of these is populated on a withdrawal response\n(`TokenWithdrawalResponse`, `NFTWithdrawalResponse`) and on\n`RetryWithdrawalResponse`, depending on the network's `Type`. Hand it to a\nwallet SDK/contract call outside this package — this SDK does not sign or\nbroadcast anything itself.\n\n```ts\n// EVM networks (Type: \"EVM\")\ninterface WithdrawalSignatureResponse {\n TokenAddress?: string;\n WalletAddress?: string;\n Amount?: string; // raw on-chain units (already scaled by decimals) — pass to the contract as-is\n BurnAmount?: string; // raw on-chain units burned by the contract; part of the signed hash for\n // withdrawERC20 — pass verbatim. Null on V1 / burn-disabled currencies.\n TokenId?: string; // NFT token id, when withdrawing an NFT (ERC-1155 id or ERC-721 tokenId)\n Nonce?: string;\n ContractAddress?: string; // the RewardPool contract to call withdrawERC20/ERC1155/ERC721 on\n UserID?: string;\n TitleID?: string; // part of the signed hash — pass on-chain verbatim\n Category?: string; // operation kind (\"game_topup\", …) — part of the signed hash, pass verbatim\n Signature?: string; // signed payload to submit to the withdrawal contract\n}\n\n// Solana networks (Type: \"Solana\")\ninterface SolanaWithdrawalSignature {\n Mint?: string;\n WalletAddress?: string;\n Amount?: string;\n Nonce?: string;\n ProgramID?: string;\n SignatureHex?: string;\n SigIxIndex?: number;\n Ed25519PublicKey?: string;\n Ed25519Message?: string;\n UserID?: string;\n}\n```\n\n---\n\n## Domain delta: BlockchainStateDelta\n\nReconciliation container for state changes NOT expressible via\n`ResourceOperation` — crypto balances are patched with a direct `$inc`\nserver-side rather than going through the shared resource pipeline, and the\npending-withdrawals list is a domain structure, not a grant/consume. It rides\nalong on the mutating responses below (`StateDelta`, optional, `null` on an\nidempotent replay — the client already applied it on the first success):\n\n```ts\ninterface BlockchainStateDelta {\n // Signed per-currency balance deltas applied by this call. Apply as\n // Amount += AmountDelta, Frozen += FrozenDelta. null if no crypto balance\n // changed (e.g. an NFT flow or a donation).\n CryptoBalances?: Record<string, CryptoBalanceChange>; // key = CurrencyID\n // A pending withdrawal added by this call (Request flows). null if none.\n PendingAdded?: PendingWithdrawalRef;\n // TitleTransactionIDs of pending withdrawals removed by this call — an\n // explicit confirm and/or lazily-expired stale ones. null/empty if none.\n PendingRemovedIDs?: string[];\n}\n\ninterface CryptoBalanceChange {\n CurrencyID?: string;\n AmountDelta?: string; // signed decimal string: + deposit, − withdrawal\n FrozenDelta?: string; // signed decimal string; 0 in current flows (withdrawal debits immediately)\n UpdatedAt?: string; // server-recorded UpdatedAt on the currency instance\n}\n```\n\nSee the SKILL.md's\n[StateDelta / Inventory](../SKILL.md#reading-state-and-reacting-to-changes)\nnote for which responses carry it and the current (manual-apply) cache\nbehavior.\n\n---\n\n## Responses\n\nMethod-by-method success shapes (see the main skill's Methods table for which\ncall returns which).\n\n```ts\ninterface BlockchainConfigResponse {\n Blockchain?: BlockchainDefinitions;\n CryptoCurrencies?: Record<string, CryptoCurrencyDefinition>; // see currency-system skill\n}\n\ninterface UserBlockchainStateResponse {\n State?: UserBlockchainState;\n CryptoBalances?: Record<string, UserCryptoCurrencyState>; // { Amount, Frozen, ... }, decimal strings\n}\n\ninterface DepositTokenResponse {\n ServerTimeUtc?: string;\n TransactionHash?: string;\n NetworkID?: string;\n CurrencyID?: string;\n AmountNative?: string; // credited amount, decimal string\n AmountUsd?: string;\n StateDelta?: BlockchainStateDelta; // crypto-balance credit\n}\n\ninterface DepositNFTResponse {\n ServerTimeUtc?: string;\n TransactionHash?: string;\n NetworkID?: string;\n ItemID?: string;\n CatalogID?: string;\n NftTokenID?: string;\n Amount?: number;\n Resources?: ResourceOperation; // see currency-system skill — already applied to cache\n Inventory?: InventoryDelta; // minted NFT's UnstackableItems instance delta; see character-system skill for the shape\n}\n\ninterface TokenWithdrawalResponse {\n ServerTimeUtc?: string;\n TitleTransactionID?: string;\n NetworkID?: string;\n CurrencyID?: string;\n AmountNative?: string; // debited (GROSS, before platform commission + burn)\n NetAmountNative?: string; // paid out on-chain (NET = GROSS − commission − burn)\n BurnAmountNative?: string; // burned on-chain for this withdrawal (0 if disabled or Solana)\n AmountUsd?: string;\n ExpiresAt?: string;\n EvmSignature?: WithdrawalSignatureResponse;\n SolanaSignature?: SolanaWithdrawalSignature;\n StateDelta?: BlockchainStateDelta; // crypto-balance debit, added pending withdrawal, lazy-expired ones\n}\n\ninterface NFTWithdrawalResponse {\n ServerTimeUtc?: string;\n TitleTransactionID?: string;\n NetworkID?: string;\n ItemID?: string;\n CatalogID?: string;\n NftTokenID?: string;\n Amount?: number;\n ExpiresAt?: string;\n Resources?: ResourceOperation; // the consumed item, already applied to cache\n EvmSignature?: WithdrawalSignatureResponse;\n SolanaSignature?: SolanaWithdrawalSignature;\n StateDelta?: BlockchainStateDelta; // added pending withdrawal, lazy-expired ones (item debit is in Resources)\n Inventory?: InventoryDelta; // withdrawn NFT's UnstackableItems instance delta (removed/reduced instances)\n}\n\ninterface TransactionHistoryResponse {\n TokenTransactions?: TokenTransactionDocument[];\n NFTTransactions?: NFTTransactionDocument[];\n}\n\ninterface RetryWithdrawalResponse {\n TitleTransactionID?: string;\n Kind?: \"Token\" | \"Nft\";\n EvmSignature?: WithdrawalSignatureResponse;\n SolanaSignature?: SolanaWithdrawalSignature;\n}\n\ninterface ConfirmWithdrawalResponse {\n TitleTransactionID?: string;\n OnChainTxHash?: string;\n Status?: \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\n StateDelta?: BlockchainStateDelta; // pending withdrawals removed (confirmed + any lazy-expired)\n}\n\ninterface DonationResponse {\n ServerTimeUtc?: string;\n TransactionHash?: string;\n NetworkID?: string;\n CurrencyID?: string;\n AmountNative?: string;\n AmountUsd?: string;\n Target?: string; // \"Developer\" or \"UsersPool\"\n}\n```\n\n---\n\n## Enums\n\n```ts\ntype BlockchainNetworkType = \"EVM\" | \"Solana\";\ntype WalletLinkType =\n \"AutoLinkedFromTransaction\" | \"SignatureVerified\" | \"ManuallyLinked\";\ntype BlockchainTransactionType = \"Token\" | \"Nft\";\ntype KycStatus =\n \"NotRequested\" | \"Pending\" | \"Verified\" | \"Rejected\" | \"Expired\";\ntype KycTier = \"None\" | \"Tier1\" | \"Tier2\" | \"Tier3\";\ntype BlockchainTransactionStatus =\n \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\ntype TransactionDirection = \"UsersCryptoWallet\" | \"Game\";\n```\n\n`WalletLinkType.SignatureVerified` and `ManuallyLinked` are modeled for\nforward compatibility but this module's methods only ever produce\n`AutoLinkedFromTransaction` today — there's no explicit \"link/verify wallet\"\ncall in `BlockchainService`. Treat the other two as reserved for a future\nsignature-based wallet-linking flow.\n"
|
|
8
|
+
"content": "# Blockchain data model — reference\n\nFull shape of the config (`BlockchainDefinitions`), player state\n(`UserBlockchainState`), transaction documents, and the withdrawal signature\npayloads. All of these are **strictly typed in the SDK** — every type below is\nexported from `@idosgames/core`, built with `zod` schemas that keep\n`.passthrough()`, so a field the backend adds later still round-trips. Field\nnames are PascalCase (straight from the backend JSON). Decimal-valued fields\n(`Amount`, balances, USD values) are decimal strings, not JS numbers — use\n`decimal.js` (already a dependency) rather than float math.\n\n## Contents\n\n- [Config: BlockchainDefinitions](#config-blockchaindefinitions) — what `getDefinitions()` returns\n- [BlockchainNetworkDefinition](#blockchainnetworkdefinition)\n- [NFT collection bindings](#nft-collection-bindings)\n- [Account safety policy](#account-safety-policy)\n- [Withdrawal gate mechanics](#withdrawal-gate-mechanics) — every check + formula the backend runs before paying out\n- [Player state: UserBlockchainState](#player-state-userblockchainstate) — what `getUserState()` returns\n- [KYC state](#kyc-state)\n- [Compliance counters](#compliance-counters) — the daily/monthly spend windows behind the limit errors\n- [Stats containers](#stats-containers)\n- [Transaction documents](#transaction-documents)\n- [Withdrawal signature payloads](#withdrawal-signature-payloads)\n- [Domain delta: BlockchainStateDelta](#domain-delta-blockchainstatedelta)\n- [Responses](#responses)\n- [Enums](#enums)\n\n---\n\n## Config: BlockchainDefinitions\n\nReturned by `getDefinitions()` as part of `BlockchainConfigResponse`; the\n`Blockchain` section is cached via\n`client.data.config.getSection<BlockchainDefinitions>(\"Blockchain\")`. The\nsibling `CryptoCurrencies` map (`Record<string, CryptoCurrencyDefinition>`)\nrides along in the same response — see the currency-system skill for that\nshape.\n\n```ts\ninterface BlockchainDefinitions {\n SystemState?: BlockchainSystemState; // title-wide kill switches\n Networks?: Record<string, BlockchainNetworkDefinition>; // key = NetworkID\n AccountSafety?: BlockchainAccountSafetyPolicy;\n}\n\ninterface BlockchainSystemState {\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n NftDepositsEnabled?: boolean;\n NftWithdrawalsEnabled?: boolean;\n PlatformOverrides?: PlatformBlockchainState; // per-platform (Ios/Android/Web) override\n}\n\ninterface PlatformBlockchainState {\n Ios?: boolean;\n Android?: boolean;\n Web?: boolean;\n}\n```\n\n`SystemState` is the title-wide switch; each `BlockchainNetworkDefinition` has\nits own matching flags that layer on top (both must allow an action for it to\nbe permitted — the backend enforces this, but mirror the check in UI to avoid\nshowing a dead button).\n\n---\n\n## BlockchainNetworkDefinition\n\nOne connected chain. Key in `Networks` is the `NetworkID` you pass to every\nservice method (`\"polygon\"`, `\"ethereum\"`, `\"solana\"`, etc. — title-defined\nstrings, not fixed by the SDK).\n\n```ts\ninterface BlockchainNetworkDefinition {\n NetworkID?: string;\n DisplayName?: string;\n Type?: \"EVM\" | \"Solana\"; // controls which signature payload shape you get back\n ChainID?: number; // EVM chain id; 0 for Solana (unused)\n ChainTicker?: string; // e.g. \"MATIC\", \"ETH\", \"SOL\" — used server-side to route RPC calls\n RewardPoolAddress?: string; // EVM: pool contract address; Solana: platform Program ID\n VaultDepositAddress?: string; // Solana-only: vault address for SPL deposits, when used\n ChainConfigVersion?: number; // default 1; controls the withdrawal signature payload format\n RequiredConfirmations?: number; // on-chain confirmations before the backend accepts a deposit; default 12\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n NftDepositsEnabled?: boolean;\n NftWithdrawalsEnabled?: boolean;\n PlatformOverrides?: PlatformBlockchainState;\n NftCollections?: BlockchainNftCollectionBinding[];\n AssetPaths?: Record<string, string>; // icons, chain logos, etc.\n}\n```\n\n`Type` determines which field is populated on withdrawal responses:\n`EvmSignature` for `\"EVM\"` networks, `SolanaSignature` for `\"Solana\"`\nnetworks. Always check `Type` (or just check which signature field is\nnon-null) rather than assuming one shape.\n\n`RequiredConfirmations` is why a `depositToken`/`depositNFT` call can fail\nright after the player submits their on-chain transaction — the backend\nwon't accept it until it has enough confirmations, returning `\"Not enough\nconfirmations (required {RequiredConfirmations}). Try again in a few\nminutes.\"` Ignored for Solana networks (finality is checked via commitment\nlevel instead). Surface \"still confirming, try again shortly\" for that\nspecific message rather than a hard failure.\n\n---\n\n## NFT collection bindings\n\nBinds one on-chain NFT contract/collection to an in-game item catalog, so the\nbackend knows which `ItemCatalogID` a deposited/withdrawn NFT maps to.\n\n```ts\ninterface BlockchainNftCollectionBinding {\n ContractAddress?: string;\n ItemCatalogID?: string; // which item catalog this contract maps to in-game\n DisplayName?: string;\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n}\n```\n\nA network can bind multiple collections (e.g. one contract for weapon NFTs,\nanother for cosmetic NFTs), each independently toggle-able.\n\n---\n\n## Account safety policy\n\n```ts\ninterface BlockchainAccountSafetyPolicy {\n MinAccountAgeDays?: number; // account must be at least this old to withdraw; default 7\n MultiAccountCheckEnabled?: boolean; // default true\n BanOnSharedWithdrawalAddress?: boolean; // default true; see below\n PendingWithdrawalTtlHours?: number; // signature validity window; default 24, min enforced 1\n}\n```\n\nRead-only/informational for the client — the backend enforces these; there's\nnothing to compute. Useful for showing a \"why is withdrawal locked\" message\n(e.g. \"Available after your account is 7 days old\"). Checked only on\nwithdrawal requests and `retryWithdrawal` — deposits are always accepted\nregardless of account age (an account can be auto-flagged from a deposit, but\nnever blocked from making one).\n\n`MultiAccountCheckEnabled` + `BanOnSharedWithdrawalAddress` together mean: if\na player requests a withdrawal to a wallet address that was already used as a\nwithdrawal _or deposit_ destination by a **different** account on this title,\nthe requesting account is **banned immediately** as part of the check (not\njust rejected) — `\"Account banned. Contact support.\"` There's no warning\nstep; a title enabling this should surface it clearly in withdrawal UI\ncopy before the player submits an address.\n\n---\n\n## Withdrawal gate mechanics\n\nThe full ordered set of server-side checks a `requestTokenWithdrawal` /\n`requestNFTWithdrawal` call goes through, with the exact backend formulas.\nThe SKILL.md's [Withdrawal gates](../SKILL.md#withdrawal-gates-what-can-reject-a-request)\nsection lists the corresponding verbatim error strings; this section is the\n\"why\" behind each one.\n\n1. **Global + per-network + per-currency + per-binding enable flags** — all\n of `BlockchainSystemState.WithdrawalsEnabled`,\n `BlockchainNetworkDefinition.WithdrawalsEnabled`,\n `CryptoCurrencyPermissions.WithdrawalsEnabled` (title-wide, all networks),\n and `CryptoNetworkBinding.WithdrawalsEnabled` (this specific\n currency+network pair) must be `true`. Any one `false` rejects the\n request — a title can pause withdrawals for one currency on one network\n (e.g. a drained hot wallet) without touching the others.\n2. **`MinWithdraw`** (`CryptoNetworkBinding.MinWithdraw`, per currency+network)\n — the requested `amount` must be `>= MinWithdraw`. There is no per-withdrawal\n fee field: what actually reduces the payout is the burn share and the pool\n commission — see the commission note below.\n3. **Balance check** — the player's `InventoryV2.CryptoCurrencies[currencyID]\n.Amount` (tokens) or owned item count (NFTs) must cover the requested\n amount.\n4. **Account safety** — see [above](#account-safety-policy).\n5. **Compliance / KYC** (`CryptoCurrencyDefinition.Limits`, all in\n USD-equivalent of the request, computed as `amountNative * ValueInUSD`):\n - `KycRequiredAboveUsd`: if the request's USD value exceeds this and\n `UserBlockchainState.Kyc.Status !== \"Verified\"`, rejected.\n - `DailyWithdrawUsd`: rejected if `DailyWithdrawnUsd (so far today) +\nthisRequestUsd > DailyWithdrawUsd`. The daily window is a fixed UTC\n calendar day (00:00 UTC), not a rolling 24h window.\n - `MonthlyWithdrawUsd`: same shape, UTC calendar month (00:00 UTC on the\n 1st).\n - Any of the three fields being absent/null on the currency disables that\n specific check.\n6. **Title-wide collective pool cap** — independent of the individual\n player's limits: the title has one `UsersWithdrawable` pool balance per\n (network, currency), fed by the remainder of every deposit after both the\n developer share (`CryptoCurrencyDefinition.DeveloperDepositSharePercent`)\n and the Community Marketing share\n (`CryptoCurrencyDefinition.CommunityMarketingDepositSharePercent`) are\n taken off the top (developer share wins on overflow if the two sum above\n 100%), plus any `donateToUsersPool` donations. A withdrawal request is\n rejected outright if `UsersWithdrawable < requestedAmount` for that pool —\n this is a platform economics limit, not a per-player one, and isn't\n exposed through any client-readable field; you only learn about it from\n the rejection.\n7. **Platform commission + EVM burn** — an operator-wide withdrawal\n commission percentage (0–100, not exposed in `BlockchainDefinitions`) is\n applied to the _gross_ requested amount, and (EVM only) a per-currency\n burn percentage (`CryptoCurrencyDefinition.WithdrawalBurnPercent`) is\n applied on top: `commission = amountNative * (commissionPercent / 100)`,\n `burn = amountNative * (WithdrawalBurnPercent / 100)` (0 on Solana),\n `net = amountNative - commission - burn`. The player is debited the full\n `amountNative` (gross); the signed payload authorizes paying out only\n `net` on-chain, with `burn` sent to the DEAD address by the contract\n itself. If `net <= 0` (commission + burn consume the whole request), the\n withdrawal is rejected before any signature is issued. This is why\n `TokenWithdrawalResponse.NetAmountNative` can be less than `AmountNative`\n — always display `NetAmountNative` as \"you'll receive,\" and\n `BurnAmountNative` if you want to show the burned portion separately. NFT\n withdrawals have no commission/burn step (no `NetAmountNative` /\n `BurnAmountNative` on `NFTWithdrawalResponse`).\n\nNone of steps 5–7 are visible ahead of time as a single client-readable\n\"can withdraw\" flag — the pattern is: attempt the call, branch on the error\nstring.\n\n---\n\n## Player state: UserBlockchainState\n\nReturned by `getUserState()` as `{ State, CryptoBalances }`\n(`UserBlockchainStateResponse`); `State` is cached at\n`client.data.user.state?.Blockchain`, `CryptoBalances` is folded into\n`client.data.user.state?.InventoryV2?.CryptoCurrencies` (same cache\n`client.data.user.getCryptoCurrencyAmount(id)` reads).\n\n```ts\ninterface UserBlockchainState {\n Version?: number;\n Stats?: BlockchainStats;\n LinkedWallets?: Record<string, LinkedWalletInfo>; // key = NetworkID\n PendingWithdrawals?: PendingWithdrawalRef[];\n Kyc?: UserKycState;\n FirstActivityAt?: string; // ISO datetime\n LastActivityAt?: string;\n IsFlagged?: boolean; // account-safety flag (see BlockchainAccountSafetyPolicy)\n FlagReason?: string;\n}\n\ninterface LinkedWalletInfo {\n NetworkID?: string;\n Address?: string;\n LinkedAt?: string;\n LastUsedAt?: string;\n LinkType?:\n \"AutoLinkedFromTransaction\" | \"SignatureVerified\" | \"ManuallyLinked\";\n IsSignatureVerified?: boolean;\n}\n\n/** Light reference only — full transaction data lives in the tx history documents. */\ninterface PendingWithdrawalRef {\n TitleTransactionID?: string;\n Type?: \"Token\" | \"Nft\";\n NetworkID?: string;\n AssetID?: string; // CurrencyID for Token withdrawals, ItemID for NFT withdrawals\n Amount?: string; // decimal string\n CreatedAt?: string;\n ExpiresAt?: string;\n}\n```\n\n`LinkedWallets` is populated automatically the first time a wallet address is\nused in a deposit/withdrawal on a network (`AutoLinkedFromTransaction`) —\nthere's no separate \"link wallet\" call in this module.\n`PendingWithdrawals` is a **light** list (id/type/asset/amount/expiry only)\nfor quickly rendering \"you have N pending withdrawals\" — cross-reference\n`TitleTransactionID` against `getTransactionHistory()` for full details\n(status, hash, fail reason).\n\n---\n\n## KYC state\n\n```ts\ninterface UserKycState {\n Status?: \"NotRequested\" | \"Pending\" | \"Verified\" | \"Rejected\" | \"Expired\";\n Tier?: \"None\" | \"Tier1\" | \"Tier2\" | \"Tier3\";\n VerifiedAt?: string;\n ExpiresAt?: string;\n RejectedAt?: string;\n ProviderReference?: string; // third-party KYC provider's reference id\n RejectionReason?: string;\n}\n```\n\nThis module surfaces KYC status for gating UI (e.g. \"verify your identity to\nwithdraw over $X\") — there's no `startKyc`/`submitKyc` method here; KYC\nverification itself happens through whatever provider integration the title\nuses outside this SDK, and this state just reflects the result.\n\n---\n\n## Compliance counters\n\nPer-currency AML spend windows that back the `\"Daily withdraw limit\nexceeded\"` / `\"Monthly withdraw limit exceeded\"` errors (see\n[Withdrawal gate mechanics](#withdrawal-gate-mechanics)). Not part of\n`UserBlockchainState` — these live alongside the balance, on each entry of\n`CryptoBalances` (the sibling map returned by `getUserState()`, cached into\n`InventoryV2.CryptoCurrencies`, read via `client.data.user\n.getCryptoCurrencyAmount(currencyID)` for the balance itself):\n\n```ts\ninterface UserCryptoComplianceCounters {\n DailyPeriodStartUtc?: string; // start of the current UTC calendar day counted\n DailyWithdrawnUsd?: string; // decimal string — USD spent so far this UTC day\n MonthlyPeriodStartUtc?: string; // start of the current UTC calendar month counted\n MonthlyWithdrawnUsd?: string; // decimal string — USD spent so far this UTC month\n}\n\ninterface UserCryptoCurrencyState {\n Amount: string; // decimal string — available balance\n Frozen: string; // decimal string — reserved by pending withdrawals\n Compliance?: UserCryptoComplianceCounters; // absent if the currency has no configured limits\n CreatedAt?: string;\n UpdatedAt?: string;\n}\n```\n\nThere is no client method to read \"USD spent so far today\" proactively — the\ncounters are internal bookkeeping the backend checks at request time and\nrolls forward automatically once the UTC day/month boundary passes (an\nexpired window resets to the new request's amount, it does not carry over).\nTreat a `\"Daily/Monthly withdraw limit exceeded (...)\"` error message as the\nonly place this data surfaces to the client, and parse the numbers out of the\nerror string if you need to show a friendlier message.\n\n---\n\n## Stats containers\n\n```ts\ninterface BlockchainStats {\n Tokens?: TokenStatsContainer;\n Nfts?: NftStatsContainer;\n}\n\ninterface TokenStatsContainer {\n TotalDeposits?: number;\n TotalWithdrawals?: number;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n TotalDepositsVolumeUsd?: string;\n TotalWithdrawalsVolumeUsd?: string;\n PerCurrency?: Record<string, TokenCurrencyStats>; // key = CurrencyID\n}\n\ninterface TokenCurrencyStats {\n CurrencyID?: string;\n Deposits?: number;\n DepositsVolumeNative?: string;\n DepositsVolumeUsd?: string;\n Withdrawals?: number;\n WithdrawalsVolumeNative?: string;\n WithdrawalsVolumeUsd?: string;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n FirstDepositAt?: string;\n LastDepositAt?: string;\n FirstWithdrawalAt?: string;\n LastWithdrawalAt?: string;\n}\n\ninterface NftStatsContainer {\n TotalDeposits?: number;\n TotalWithdrawals?: number;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n PerCollection?: Record<string, NftCollectionStats>; // key = ItemCatalogID (or composite id)\n}\n\ninterface NftCollectionStats {\n NetworkID?: string;\n ItemCatalogID?: string;\n Deposits?: number;\n Withdrawals?: number;\n FailedWithdrawals?: number;\n RejectedDeposits?: number;\n FirstDepositAt?: string;\n LastDepositAt?: string;\n FirstWithdrawalAt?: string;\n LastWithdrawalAt?: string;\n}\n```\n\nLifetime counters/volumes for a player's own activity — handy for a \"your\non-chain activity\" summary screen. Purely informational; nothing to act on.\n\n---\n\n## Transaction documents\n\nReturned by `getTransactionHistory()` as `{ TokenTransactions, NFTTransactions }`\n(`TransactionHistoryResponse`). These are the full records — richer than the\nlight `PendingWithdrawalRef`. Both arrays are capped to the same `limit`\n(default 50, hard server-side ceiling 200 — values above 200 are silently\nclamped, values `<= 0` fall back to the default of 50); there's no separate\nper-type limit or pagination cursor.\n\n```ts\ninterface TokenTransactionDocument {\n ID?: string;\n TitleID?: string;\n CreatedAt?: string;\n UpdatedAt?: string;\n UserID?: string;\n TransactionHash?: string; // on-chain hash once known\n Nonce?: string;\n NetworkID?: string;\n ChainType?: string;\n ChainID?: number;\n Direction?: \"UsersCryptoWallet\" | \"Game\"; // which way the asset moved\n From?: string;\n To?: string;\n Amount?: string;\n Status?: \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\n SignatureData?: string;\n CompletedAt?: string;\n ExpiresAt?: string;\n FailReason?: string;\n Reason?: string;\n Category?: string; // operation kind (\"game_topup\", \"community_reward\", …); V2-only, absent on V1 txs\n TokenID?: string;\n CurrencyID?: string;\n AmountUsd?: string;\n NetPayoutAmount?: string; // withdrawals only: amount after platform commission\n}\n\ninterface NFTTransactionDocument {\n ID?: string;\n TitleID?: string;\n CreatedAt?: string;\n UpdatedAt?: string;\n UserID?: string;\n TransactionHash?: string;\n Nonce?: string;\n NetworkID?: string;\n ChainType?: string;\n ChainID?: number;\n Direction?: \"UsersCryptoWallet\" | \"Game\";\n From?: string;\n To?: string;\n Amount?: string;\n Status?: \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\n SignatureData?: string;\n CompletedAt?: string;\n ExpiresAt?: string;\n FailReason?: string;\n Reason?: string;\n Category?: string; // operation kind (\"game_topup\", \"community_reward\", …); V2-only, absent on V1 txs\n NFTID?: string;\n ItemID?: string;\n CatalogID?: string;\n SkinID?: string;\n}\n```\n\n`Direction: \"UsersCryptoWallet\"` = a withdrawal (asset moving to the player's\nwallet); `Direction: \"Game\"` = a deposit (asset moving into the game). `Status`\nis the authoritative lifecycle value for a transaction — cross-reference it\nagainst `PendingWithdrawalRef` (state) or the response you got from\n`requestTokenWithdrawal`/`requestNFTWithdrawal` (by `TitleTransactionID` ==\n`ID`) to know exactly where a withdrawal is:\n\n| Status | Meaning |\n| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Pending` | Requested/signed but not yet confirmed on-chain. |\n| `Completed` | Confirmed on-chain (`confirmWithdrawal` succeeded and chain verified). |\n| `Failed` | Rejected — see `FailReason`. |\n| `Expired` | Modeled in the enum but not currently assigned by this backend path. |\n| `Abandoned` | TTL (`ExpiresAt`) passed without submission — this is the status a lazily-expired pending withdrawal actually lands on, not `Expired`. Can still be closed out by a late `confirmWithdrawal` if the player submitted on-chain before the backend swept it (see the SKILL.md gotcha). |\n\nNote the divergence from what the field name suggests: **`retryWithdrawal`\nonly accepts a transaction currently in `Pending`** — it rejects\n`Failed`/`Abandoned`/`Completed` alike with `\"Transaction is not in Pending\nstate (current: {status}).\"` (see the SKILL.md's\n[Gotchas](../SKILL.md#gotchas) section for the full retry/confirm lifecycle).\nIn practice a TTL-expired withdrawal (now `Abandoned`) is **not** retryable\nthrough `retryWithdrawal` — the only path forward for one is\n`confirmWithdrawal` with a hash, if the player actually submitted the\noriginal signature before it was swept.\n\n---\n\n## Withdrawal signature payloads\n\nExactly one of these is populated on a withdrawal response\n(`TokenWithdrawalResponse`, `NFTWithdrawalResponse`) and on\n`RetryWithdrawalResponse`, depending on the network's `Type`. Hand it to a\nwallet SDK/contract call outside this package — this SDK does not sign or\nbroadcast anything itself.\n\n```ts\n// EVM networks (Type: \"EVM\")\ninterface WithdrawalSignatureResponse {\n TokenAddress?: string;\n WalletAddress?: string;\n Amount?: string; // raw on-chain units (already scaled by decimals) — pass to the contract as-is\n BurnAmount?: string; // raw on-chain units burned by the contract; part of the signed hash for\n // withdrawERC20 — pass verbatim. Null on V1 / burn-disabled currencies.\n TokenId?: string; // NFT token id, when withdrawing an NFT (ERC-1155 id or ERC-721 tokenId)\n Nonce?: string;\n ContractAddress?: string; // the RewardPool contract to call withdrawERC20/ERC1155/ERC721 on\n UserID?: string;\n TitleID?: string; // part of the signed hash — pass on-chain verbatim\n Category?: string; // operation kind (\"game_topup\", …) — part of the signed hash, pass verbatim\n Signature?: string; // signed payload to submit to the withdrawal contract\n}\n\n// Solana networks (Type: \"Solana\")\ninterface SolanaWithdrawalSignature {\n Mint?: string;\n WalletAddress?: string;\n Amount?: string;\n Nonce?: string;\n ProgramID?: string;\n SignatureHex?: string;\n Ed25519PublicKey?: string;\n Ed25519Message?: string;\n UserID?: string;\n}\n```\n\n---\n\n## Domain delta: BlockchainStateDelta\n\nReconciliation container for state changes NOT expressible via\n`ResourceOperation` — crypto balances are patched with a direct `$inc`\nserver-side rather than going through the shared resource pipeline, and the\npending-withdrawals list is a domain structure, not a grant/consume. It rides\nalong on the mutating responses below (`StateDelta`, optional, `null` on an\nidempotent replay — the client already applied it on the first success):\n\n```ts\ninterface BlockchainStateDelta {\n // Signed per-currency balance deltas applied by this call. Apply as\n // Amount += AmountDelta, Frozen += FrozenDelta. null if no crypto balance\n // changed (e.g. an NFT flow or a donation).\n CryptoBalances?: Record<string, CryptoBalanceChange>; // key = CurrencyID\n // A pending withdrawal added by this call (Request flows). null if none.\n PendingAdded?: PendingWithdrawalRef;\n // TitleTransactionIDs of pending withdrawals removed by this call — an\n // explicit confirm and/or lazily-expired stale ones. null/empty if none.\n PendingRemovedIDs?: string[];\n}\n\ninterface CryptoBalanceChange {\n CurrencyID?: string;\n AmountDelta?: string; // signed decimal string: + deposit, − withdrawal\n FrozenDelta?: string; // signed decimal string; 0 in current flows (withdrawal debits immediately)\n UpdatedAt?: string; // server-recorded UpdatedAt on the currency instance\n}\n```\n\nSee the SKILL.md's\n[StateDelta / Inventory](../SKILL.md#reading-state-and-reacting-to-changes)\nnote for which responses carry it and the current (manual-apply) cache\nbehavior.\n\n---\n\n## Responses\n\nMethod-by-method success shapes (see the main skill's Methods table for which\ncall returns which).\n\n```ts\ninterface BlockchainConfigResponse {\n Blockchain?: BlockchainDefinitions;\n CryptoCurrencies?: Record<string, CryptoCurrencyDefinition>; // see currency-system skill\n}\n\ninterface UserBlockchainStateResponse {\n State?: UserBlockchainState;\n CryptoBalances?: Record<string, UserCryptoCurrencyState>; // { Amount, Frozen, ... }, decimal strings\n}\n\ninterface DepositTokenResponse {\n ServerTimeUtc?: string;\n TransactionHash?: string;\n NetworkID?: string;\n CurrencyID?: string;\n AmountNative?: string; // credited amount, decimal string\n AmountUsd?: string;\n StateDelta?: BlockchainStateDelta; // crypto-balance credit\n}\n\ninterface DepositNFTResponse {\n ServerTimeUtc?: string;\n TransactionHash?: string;\n NetworkID?: string;\n ItemID?: string;\n CatalogID?: string;\n NftTokenID?: string;\n Amount?: number;\n Resources?: ResourceOperation; // see currency-system skill — already applied to cache\n Inventory?: InventoryDelta; // minted NFT's UnstackableItems instance delta; see character-system skill for the shape\n}\n\ninterface TokenWithdrawalResponse {\n ServerTimeUtc?: string;\n TitleTransactionID?: string;\n NetworkID?: string;\n CurrencyID?: string;\n AmountNative?: string; // debited (GROSS, before platform commission + burn)\n NetAmountNative?: string; // paid out on-chain (NET = GROSS − commission − burn)\n BurnAmountNative?: string; // burned on-chain for this withdrawal (0 if disabled or Solana)\n AmountUsd?: string;\n ExpiresAt?: string;\n EvmSignature?: WithdrawalSignatureResponse;\n SolanaSignature?: SolanaWithdrawalSignature;\n StateDelta?: BlockchainStateDelta; // crypto-balance debit, added pending withdrawal, lazy-expired ones\n}\n\ninterface NFTWithdrawalResponse {\n ServerTimeUtc?: string;\n TitleTransactionID?: string;\n NetworkID?: string;\n ItemID?: string;\n CatalogID?: string;\n NftTokenID?: string;\n Amount?: number;\n ExpiresAt?: string;\n Resources?: ResourceOperation; // the consumed item, already applied to cache\n EvmSignature?: WithdrawalSignatureResponse;\n SolanaSignature?: SolanaWithdrawalSignature;\n StateDelta?: BlockchainStateDelta; // added pending withdrawal, lazy-expired ones (item debit is in Resources)\n Inventory?: InventoryDelta; // withdrawn NFT's UnstackableItems instance delta (removed/reduced instances)\n}\n\ninterface TransactionHistoryResponse {\n TokenTransactions?: TokenTransactionDocument[];\n NFTTransactions?: NFTTransactionDocument[];\n}\n\ninterface RetryWithdrawalResponse {\n TitleTransactionID?: string;\n Kind?: \"Token\" | \"Nft\";\n EvmSignature?: WithdrawalSignatureResponse;\n SolanaSignature?: SolanaWithdrawalSignature;\n}\n\ninterface ConfirmWithdrawalResponse {\n TitleTransactionID?: string;\n OnChainTxHash?: string;\n Status?: \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\n StateDelta?: BlockchainStateDelta; // pending withdrawals removed (confirmed + any lazy-expired)\n}\n\ninterface DonationResponse {\n ServerTimeUtc?: string;\n TransactionHash?: string;\n NetworkID?: string;\n CurrencyID?: string;\n AmountNative?: string;\n AmountUsd?: string;\n Target?: string; // \"Developer\" or \"UsersPool\"\n}\n```\n\n---\n\n## Enums\n\n```ts\ntype BlockchainNetworkType = \"EVM\" | \"Solana\";\ntype WalletLinkType =\n \"AutoLinkedFromTransaction\" | \"SignatureVerified\" | \"ManuallyLinked\";\ntype BlockchainTransactionType = \"Token\" | \"Nft\";\ntype KycStatus =\n \"NotRequested\" | \"Pending\" | \"Verified\" | \"Rejected\" | \"Expired\";\ntype KycTier = \"None\" | \"Tier1\" | \"Tier2\" | \"Tier3\";\ntype BlockchainTransactionStatus =\n \"Pending\" | \"Completed\" | \"Failed\" | \"Expired\" | \"Abandoned\";\ntype TransactionDirection = \"UsersCryptoWallet\" | \"Game\";\n```\n\n`WalletLinkType.SignatureVerified` and `ManuallyLinked` are modeled for\nforward compatibility but this module's methods only ever produce\n`AutoLinkedFromTransaction` today — there's no explicit \"link/verify wallet\"\ncall in `BlockchainService`. Treat the other two as reserved for a future\nsignature-based wallet-linking flow.\n"
|
|
9
9
|
}
|
|
10
10
|
]
|
|
11
11
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "community-marketing-system",
|
|
3
3
|
"description": "Add a creator reward programme (\"community marketing\") to a game on the iDosGames TypeScript SDK (@idosgames/core) via client.communityMarketing (CommunityMarketingService): show whether the title runs a programme, send the player to the creator portal, read what they earned for posting videos / clips / screenshots to YouTube, TikTok or Instagram, show the per-work breakdown of that number, and claim approved rewards in game currency or crypto. Use this whenever the user is working in the iDosGames TS SDK or its game templates and wants creator rewards, an ambassador or influencer programme, \"get paid for posting about the game\", UGC bounties, view-based payouts, a creator dashboard inside the game, or otherwise touches client.communityMarketing, CommunityMarketingService, CommunityMarketingState, CommunityEarningsResponse or CommunityClaimResponse — even if they don't name the module explicitly.",
|
|
4
|
-
"content": "---\nname: community-marketing-system\ndescription: >-\n Add a creator reward programme (\"community marketing\") to a game on the\n iDosGames TypeScript SDK (@idosgames/core) via client.communityMarketing\n (CommunityMarketingService): show whether the title runs a programme, send\n the player to the creator portal, read what they earned for posting videos /\n clips / screenshots to YouTube, TikTok or Instagram, show the per-work\n breakdown of that number, and claim approved rewards in game currency or\n crypto. Use this whenever the user is working in the iDosGames TS SDK or its\n game templates and wants creator rewards, an ambassador or influencer\n programme, \"get paid for posting about the game\", UGC bounties, view-based\n payouts, a creator dashboard inside the game, or otherwise touches\n client.communityMarketing, CommunityMarketingService,\n CommunityMarketingState, CommunityEarningsResponse or CommunityClaimResponse\n — even if they don't name the module explicitly.\n---\n\n# Community Marketing (iDosGames TS SDK)\n\nA **creator reward programme**: community members post videos, clips or\nslideshows about the title on social platforms, the platform reads the public\nmetrics of those posts, and the creator gets paid — in the title's currency,\nan item, or crypto — according to rates and milestones the publisher set.\n\n**The game owns almost none of this**, and that is the single most important\nthing to understand before writing any UI.\n\n## What lives where\n\n| Step | Where it happens |\n| ----------------------------------------------------- | ----------------------------- |\n| Joining the programme, submitting a post link | Creator portal on the website |\n| Moderation, metric polling, anti-fraud, accrual maths | Management backend |\n| **Showing state, showing earnings, claiming money** | **The game — this module** |\n\nSo `client.communityMarketing` has exactly three calls. If you find yourself\nwanting a fourth — \"submit a video from the game\", \"approve a submission\" —\nstop: that surface does not exist and must not be recreated. Send the player to\nthe portal instead.\n\n```ts\nconst state = await client.communityMarketing.getState();\nif (isOk(state) && state.data.IsEnabled) {\n // ⚠ OPEN this URL. Never build one from a template.\n openExternal(state.data.CreatorPortalUrl!);\n}\n```\n\n## The three calls\n\n### `getState()`\n\nIs the programme on, is this player a creator, and where to send them.\n\n```ts\nconst res = await client.communityMarketing.getState();\nif (!isOk(res)) return;\n\nconst {\n IsEnabled,\n CreatorPortalUrl,\n ParticipationMode, // \"Open\" | \"Application\"\n IsCreator,\n CreatorStatus, // Pending | Approved | Rejected | Suspended | Left\n VerificationCode,\n Campaigns, // [{ CampaignID, DisplayNameKey, IconPath, IsRunning }]\n} = res.data;\n```\n\n- Answers with a **meaningful empty state**, not an error, for a player who\n never joined — that is the normal case for everyone opening the screen. Do\n not render an error banner for `IsCreator: false`.\n- `DisplayNameKey` / `DescriptionKey` are **localization keys**: wrap them in\n `client.localization.t()`. A title that never set up localization gets the\n literal back, so this is always safe.\n- `VerificationCode` is the creator's proof-of-authorship code — it only ever\n comes back to its owner. Showing it in-game is useful (they need it in the\n post description), but the post itself is submitted in the portal.\n\n### `getEarnings(limit?)`\n\nWhat the player earned, **why**, and the payout history.\n\n```ts\nconst res = await client.communityMarketing.getEarnings();\nif (!isOk(res)) return;\n\nconst { HoldDays, Campaigns, Payouts } = res.data;\n\nfor (const line of Campaigns ?? []) {\n line.Earned; // everything the formula has counted — an ESTIMATE, and it can go DOWN\n line.Mature; // the part past the hold period — still an estimate\n line.Granted; // ⚠ THE PROMISE: what a moderator approved for payout\n line.Paid; // already handed over\n line.Claimable; // what the button will actually pay, in WHOLE units, out of Granted\n line.BySubmission; // per-work breakdown: { [submissionID]: { Earned, Mature, CountedMetrics, ... } }\n}\n```\n\n⚠⚠ **`Earned` is an estimate; `Granted` is the promise.** The formula recomputes\nfrom metric snapshots every time, so `Earned` grows with views and **drops** when\nthe platform scrubs fake activity. It becomes money owed only once a human\napproves it. Showing only `Earned` is how this screen turns into a support\nticket — one day the number goes down with nothing on screen to explain it:\n\n| Number | Means |\n| ----------- | -------------------------------------------------------- |\n| `Earned` | what the current metrics are worth — moves both ways |\n| `Mature` | the part of it older than `HoldDays` — still an estimate |\n| `Granted` | **approved by a moderator; this is what gets paid** |\n| `Paid` | what has already been handed over |\n| `Claimable` | `floor(Granted − Paid)` — what the button pays now |\n\n⚠ **`Mature − Granted > 0` is a normal state, not a delay to hide.** It means\nthe work has matured and is waiting on a human. Say that: otherwise the creator\nsees two numbers that do not add up and no one to ask about it.\n\n`HoldDays` exists so the UI can **say why** maturing takes time: social\nplatforms strip fake activity in the first days, so paying immediately would\nmean paying for it. Show that sentence. A number that quietly refuses to be\nclaimable reads as a bug.\n\n⚠ **`Overpaid > 0` is not an error.** A metric fell after the player was\nalready paid for it. The platform never takes it back — future earnings absorb\nit. Say so, or `Mature < Paid` looks like broken arithmetic.\n\n⚠ **Render `BySubmission`.** The programme pays for someone's work by a formula\nthey never saw. A total with no \"which video, at what rate\" is\nindistinguishable from an arbitrary number.\n\n### `claim(campaignID)`\n\nHands over the **approved** reward in game currency or items. It lands in the\nordinary balance through the same path as any other reward.\n\n⚠ **Crypto campaigns are refused here** with `USE_WITHDRAW_FOR_CRYPTO` — see\n`withdrawCryptoReward` below.\n\n```ts\nconst res = await client.communityMarketing.claim(campaignID);\nif (!isOk(res)) return;\n\nconst { Claimed, CurrencyID, RemainingFraction, IdempotentReplay, Message } =\n res.data;\n```\n\n⚠ **`Claimed: 0` is a SUCCESS.** Payouts are whole units, so a creator holding\n0.4 currency gets zero and keeps the 0.4 in `RemainingFraction`. Show that\nnumber — without it the player concludes their earnings were eaten. `Message`\ncarries the server's reason for the zero.\n\n⚠ **`IdempotentReplay: true` means the money moved on an earlier attempt.** A\nretried claim is safe and never double-pays. Say \"already paid\", not \"paid\nagain\", and do not add the amount to the balance a second time.\n\n### `withdrawCryptoReward(campaignID, walletAddress, amount?)`\n\nCrypto rewards only. Sends the approved amount **out of the programme's pool\nbucket straight to the wallet** — it never passes through the in-game balance.\n\n```ts\nconst res = await client.communityMarketing.withdrawCryptoReward(\n campaignID,\n walletAddress,\n);\nif (!isOk(res)) return;\n\nconst {\n TitleTransactionID,\n ChainID,\n EvmSignature,\n SolanaSignature,\n NetAmountNative,\n} = res.data;\n\n// EVM — with @idosgames/wallet:\n// const hash = await submitEvmTokenWithdrawal(clients, EvmSignature);\n// await client.blockchain.confirmWithdrawal({ TitleTransactionID, TransactionHash: hash });\n\n// Solana — build `withdraw_spl` against the platform_pool program, ed25519 pre-instruction FIRST:\n// Ed25519Program.createInstructionWithPublicKey({ publicKey, message, signature }) // ← see below\n// ...then confirmWithdrawal the same way.\n```\n\n⚠⚠ **On Solana, do NOT re-encode the voucher message.** `SolanaSignature.Ed25519Message` is the\nexact byte string the server signed — hand those bytes to the precompile. The `WithdrawSigMessage`\nlayout is a cross-repo contract between the program, its SDK and two backends, and it has already\ndrifted once (`expires_at` was added, not every copy updated, the decoder silently died). Encoding\nthe instruction ARGUMENTS yourself is fine and unavoidable: the program rebuilds the signed message\nfrom them and compares, so a mistake yields `SigMismatch` — a rejected transaction, never a wrong\npayment.\n\n⚠ Solana specifics that cost real money to rediscover: the ed25519 instruction must be **first**\n(`SigIxIndex` points at it); every split recipient's ATA must exist (create idempotently, in the\nsame order as `Splits`); the token program comes from the **mint's owner** (Token-2022 lives\nelsewhere and the ATA addresses follow it); and browser wallets are safest with a **legacy**\ntransaction — not every adapter supports v0.\n\n⚠⚠ **Why this is a separate call and not a flavour of `claim`.** In-game crypto\nand Community Marketing money are different money with opposite rules. A player's\nin-game crypto balance promises no payout — if the pool is empty they simply\ncannot withdraw, and that is by design. A creator whose work was approved is owed\nthe money regardless. Put both in one balance and you lose both rules at once.\n\n⚠⚠ **The pool is debited and the request exists the moment this resolves**,\nbefore anything reaches the chain — exactly like an ordinary withdrawal. So\n\"it failed, press it again\" charges a **second** time. Keep `TitleTransactionID`\nand finish an interrupted one with `confirmWithdrawal` (if the transaction\nactually landed) or `retryWithdrawal`. Never ask for a fresh voucher.\n\n⚠ **`ChainID` comes from the server, next to the signature — do not derive it.**\n`NetworkID` is a name (`\"bsc\"`); the signature is bound to the number. Sent to\nthe wrong chain the transaction is not misdelivered, it is rejected — after\nspending gas. Solana carries `ProgramID` in the voucher for the same reason.\n\n⚠ **Which chain family a reward pays on is the server's answer, not a guess.** Publishers name\ntheir own networks, so `NetworkID` alone tells you nothing; the creator-facing programme view\ncarries the family explicitly. Guessing here means offering the wrong wallet.\n\n⚠ **`NetAmountNative` is what reaches the wallet**, `AmountNative` is what left\nthe pool. Commission and burn are the difference; show the one you mean.\n\n### `checkJoinEligibility()`\n\nEvaluates the publisher's join requirements against the signed-in player and, if they hold, joins\nthe programme.\n\n```ts\nconst res = await client.communityMarketing.checkJoinEligibility();\nif (!isOk(res)) return;\n\nconst { Passed, Approved, Conditions, WalletMissing } = res.data;\n```\n\n⚠ Only meaningful while the programme's participation mode is `Automatic` and the player has\nalready applied — the application itself is created in the creator portal. In the by-application\nmode the check still reports the breakdown but never admits anyone.\n\n⚠⚠ **It never overrides a human.** A creator the publisher rejected or suspended stays that way\neven with every condition satisfied — `Passed: true, Approved: false` is exactly that case, and the\nUI should say so rather than showing a silent no-op.\n\n⚠ **Render `Conditions`, including on success.** The programme pays for work by rules the person\nnever saw; a bare \"not allowed\" reads as arbitrary, while \"1000 GOLD needed, you have 400\" reads as\na rule they can meet. Showing it on success too tells them what they would lose by dropping below.\n\n⚠ **`Actual: null` means NOT MEASURED, not zero** — the condition was skipped because the outcome\nwas already decided, or the wallet balance could not be read. Rendering it as zero tells the player\nsomething false about their own balance.\n\n## Events\n\n```ts\nclient.on(\"communityMarketing:stateLoaded\", (state) => {});\nclient.on(\"communityMarketing:earningsLoaded\", (earnings) => {});\nclient.on(\"communityMarketing:claimed\", (claim) => {});\nclient.on(\"communityMarketing:withdrawn\", (voucher) => {});\nclient.on(\"communityMarketing:eligibilityChecked\", (verdict) => {});\n```\n\n⚠ `communityMarketing:withdrawn` fires when the **voucher is issued** — the pool\nis already debited, but nothing has reached the chain yet. It means \"payout\nstarted\", not \"money received\".\n\n⚠ `communityMarketing:claimed` fires on **any** successful claim — including\none that paid zero and one that was an idempotent replay. Check `Claimed` and\n`IdempotentReplay` before playing a reward animation.\n\n## A minimal screen\n\n```tsx\nconst [state, setState] = useState<CommunityMarketingState | null>(null);\nconst [earnings, setEarnings] = useState<CommunityEarningsResponse | null>(\n null,\n);\n\nuseEffect(() => {\n void (async () => {\n const s = await client.communityMarketing.getState();\n if (!isOk(s) || !s.data.IsEnabled) return;\n setState(s.data);\n\n // Earnings only make sense for someone actually in the programme.\n if (s.data.IsCreator) {\n const e = await client.communityMarketing.getEarnings();\n if (isOk(e)) setEarnings(e.data);\n }\n })();\n}, []);\n```\n\n- Not enabled → render nothing. A programme the publisher never set up should\n not leave a dead menu entry.\n- Enabled, not a creator → one button: open `CreatorPortalUrl`.\n- Creator → the four numbers (`Earned` / `Mature` / `Granted` / `Paid`), the\n breakdown, and per campaign either a Claim button (game currency) or a\n Withdraw-to-wallet button (crypto).\n\n## Do not\n\n- **Do not poll.** Metrics are sampled by a backend job on a slow schedule\n (hours, not seconds). Refresh on screen open and after a claim; a polling\n loop burns the publisher's API quota to re-read a number that did not move.\n- **Do not build `CreatorPortalUrl`.** It is a server contract; a locally\n assembled address silently 404s the day the route changes.\n- **Do not treat a rejected claim as a client bug.** Fund exhausted, hold\n period, per-creator cap, suspended participation — all of these are the\n backend enforcing the publisher's rules. Surface the message.\n- **Do not implement submission, moderation or metric reading.** They belong to\n the portal and the management backend, and a second implementation would be a\n second place able to create value.\n",
|
|
4
|
+
"content": "---\nname: community-marketing-system\ndescription: >-\n Add a creator reward programme (\"community marketing\") to a game on the\n iDosGames TypeScript SDK (@idosgames/core) via client.communityMarketing\n (CommunityMarketingService): show whether the title runs a programme, send\n the player to the creator portal, read what they earned for posting videos /\n clips / screenshots to YouTube, TikTok or Instagram, show the per-work\n breakdown of that number, and claim approved rewards in game currency or\n crypto. Use this whenever the user is working in the iDosGames TS SDK or its\n game templates and wants creator rewards, an ambassador or influencer\n programme, \"get paid for posting about the game\", UGC bounties, view-based\n payouts, a creator dashboard inside the game, or otherwise touches\n client.communityMarketing, CommunityMarketingService,\n CommunityMarketingState, CommunityEarningsResponse or CommunityClaimResponse\n — even if they don't name the module explicitly.\n---\n\n# Community Marketing (iDosGames TS SDK)\n\nA **creator reward programme**: community members post videos, clips or\nslideshows about the title on social platforms, the platform reads the public\nmetrics of those posts, and the creator gets paid — in the title's currency,\nan item, or crypto — according to rates and milestones the publisher set.\n\n**The game owns almost none of this**, and that is the single most important\nthing to understand before writing any UI.\n\n## What lives where\n\n| Step | Where it happens |\n| ----------------------------------------------------- | ----------------------------- |\n| Joining the programme, submitting a post link | Creator portal on the website |\n| Moderation, metric polling, anti-fraud, accrual maths | Management backend |\n| **Showing state, showing earnings, claiming money** | **The game — this module** |\n\nSo `client.communityMarketing` has exactly three calls. If you find yourself\nwanting a fourth — \"submit a video from the game\", \"approve a submission\" —\nstop: that surface does not exist and must not be recreated. Send the player to\nthe portal instead.\n\n```ts\nconst state = await client.communityMarketing.getState();\nif (isOk(state) && state.data.IsEnabled) {\n // ⚠ OPEN this URL. Never build one from a template.\n openExternal(state.data.CreatorPortalUrl!);\n}\n```\n\n## The three calls\n\n### `getState()`\n\nIs the programme on, is this player a creator, and where to send them.\n\n```ts\nconst res = await client.communityMarketing.getState();\nif (!isOk(res)) return;\n\nconst {\n IsEnabled,\n CreatorPortalUrl,\n ParticipationMode, // \"Open\" | \"Application\"\n IsCreator,\n CreatorStatus, // Pending | Approved | Rejected | Suspended | Left\n VerificationCode,\n Campaigns, // [{ CampaignID, DisplayNameKey, IconPath, IsRunning }]\n} = res.data;\n```\n\n- Answers with a **meaningful empty state**, not an error, for a player who\n never joined — that is the normal case for everyone opening the screen. Do\n not render an error banner for `IsCreator: false`.\n- `DisplayNameKey` / `DescriptionKey` are **localization keys**: wrap them in\n `client.localization.t()`. A title that never set up localization gets the\n literal back, so this is always safe.\n- `VerificationCode` is the creator's proof-of-authorship code — it only ever\n comes back to its owner. Showing it in-game is useful (they need it in the\n post description), but the post itself is submitted in the portal.\n\n### `getEarnings(limit?)`\n\nWhat the player earned, **why**, and the payout history.\n\n```ts\nconst res = await client.communityMarketing.getEarnings();\nif (!isOk(res)) return;\n\nconst { HoldDays, Campaigns, Payouts } = res.data;\n\nfor (const line of Campaigns ?? []) {\n line.Earned; // everything the formula has counted — an ESTIMATE, and it can go DOWN\n line.Mature; // the part past the hold period — still an estimate\n line.Granted; // ⚠ THE PROMISE: what a moderator approved for payout\n line.Paid; // already handed over\n line.Claimable; // what the button will actually pay, in WHOLE units, out of Granted\n line.BySubmission; // per-work breakdown: { [submissionID]: { Earned, Mature, CountedMetrics, ... } }\n}\n```\n\n⚠⚠ **`Earned` is an estimate; `Granted` is the promise.** The formula recomputes\nfrom metric snapshots every time, so `Earned` grows with views and **drops** when\nthe platform scrubs fake activity. It becomes money owed only once a human\napproves it. Showing only `Earned` is how this screen turns into a support\nticket — one day the number goes down with nothing on screen to explain it:\n\n| Number | Means |\n| ----------- | -------------------------------------------------------- |\n| `Earned` | what the current metrics are worth — moves both ways |\n| `Mature` | the part of it older than `HoldDays` — still an estimate |\n| `Granted` | **approved by a moderator; this is what gets paid** |\n| `Paid` | what has already been handed over |\n| `Claimable` | `floor(Granted − Paid)` — what the button pays now |\n\n⚠ **`Mature − Granted > 0` is a normal state, not a delay to hide.** It means\nthe work has matured and is waiting on a human. Say that: otherwise the creator\nsees two numbers that do not add up and no one to ask about it.\n\n`HoldDays` exists so the UI can **say why** maturing takes time: social\nplatforms strip fake activity in the first days, so paying immediately would\nmean paying for it. Show that sentence. A number that quietly refuses to be\nclaimable reads as a bug.\n\n⚠ **`Overpaid > 0` is not an error.** A metric fell after the player was\nalready paid for it. The platform never takes it back — future earnings absorb\nit. Say so, or `Mature < Paid` looks like broken arithmetic.\n\n⚠ **Render `BySubmission`.** The programme pays for someone's work by a formula\nthey never saw. A total with no \"which video, at what rate\" is\nindistinguishable from an arbitrary number.\n\n### `claim(campaignID)`\n\nHands over the **approved** reward in game currency or items. It lands in the\nordinary balance through the same path as any other reward.\n\n⚠ **Crypto campaigns are refused here** with `USE_WITHDRAW_FOR_CRYPTO` — see\n`withdrawCryptoReward` below.\n\n```ts\nconst res = await client.communityMarketing.claim(campaignID);\nif (!isOk(res)) return;\n\nconst { Claimed, CurrencyID, RemainingFraction, IdempotentReplay, Message } =\n res.data;\n```\n\n⚠ **`Claimed: 0` is a SUCCESS.** Payouts are whole units, so a creator holding\n0.4 currency gets zero and keeps the 0.4 in `RemainingFraction`. Show that\nnumber — without it the player concludes their earnings were eaten. `Message`\ncarries the server's reason for the zero.\n\n⚠ **`IdempotentReplay: true` means the money moved on an earlier attempt.** A\nretried claim is safe and never double-pays. Say \"already paid\", not \"paid\nagain\", and do not add the amount to the balance a second time.\n\n### `withdrawCryptoReward(campaignID, walletAddress, amount?)`\n\nCrypto rewards only. Sends the approved amount **out of the programme's pool\nbucket straight to the wallet** — it never passes through the in-game balance.\n\n```ts\nconst res = await client.communityMarketing.withdrawCryptoReward(\n campaignID,\n walletAddress,\n);\nif (!isOk(res)) return;\n\nconst {\n TitleTransactionID,\n ChainID,\n EvmSignature,\n SolanaSignature,\n NetAmountNative,\n} = res.data;\n\n// EVM — with @idosgames/wallet:\n// const hash = await submitEvmTokenWithdrawal(clients, EvmSignature);\n// await client.blockchain.confirmWithdrawal({ TitleTransactionID, TransactionHash: hash });\n\n// Solana — build `withdraw_spl` against the platform_pool program, ed25519 pre-instruction FIRST:\n// Ed25519Program.createInstructionWithPublicKey({ publicKey, message, signature }) // ← see below\n// ...then confirmWithdrawal the same way.\n```\n\n⚠⚠ **On Solana, do NOT re-encode the voucher message.** `SolanaSignature.Ed25519Message` is the\nexact byte string the server signed — hand those bytes to the precompile. The `WithdrawSigMessage`\nlayout is a cross-repo contract between the program, its SDK and two backends, and it has already\ndrifted once (`expires_at` was added, not every copy updated, the decoder silently died). Encoding\nthe instruction ARGUMENTS yourself is fine and unavoidable: the program rebuilds the signed message\nfrom them and compares, so a mistake yields `SigMismatch` — a rejected transaction, never a wrong\npayment.\n\n⚠ Solana specifics that cost real money to rediscover: the ed25519 instruction may stand anywhere —\nthe program finds it itself (there is no `sig_ix_index` argument; wallets such as MetaMask reorder\ninstructions); every split recipient's ATA must exist (create idempotently, in the\nsame order as `Splits`); the token program comes from the **mint's owner** (Token-2022 lives\nelsewhere and the ATA addresses follow it); and browser wallets are safest with a **legacy**\ntransaction — not every adapter supports v0.\n\n⚠⚠ **Why this is a separate call and not a flavour of `claim`.** In-game crypto\nand Community Marketing money are different money with opposite rules. A player's\nin-game crypto balance promises no payout — if the pool is empty they simply\ncannot withdraw, and that is by design. A creator whose work was approved is owed\nthe money regardless. Put both in one balance and you lose both rules at once.\n\n⚠⚠ **The pool is debited and the request exists the moment this resolves**,\nbefore anything reaches the chain — exactly like an ordinary withdrawal. So\n\"it failed, press it again\" charges a **second** time. Keep `TitleTransactionID`\nand finish an interrupted one with `confirmWithdrawal` (if the transaction\nactually landed) or `retryWithdrawal`. Never ask for a fresh voucher.\n\n⚠ **`ChainID` comes from the server, next to the signature — do not derive it.**\n`NetworkID` is a name (`\"bsc\"`); the signature is bound to the number. Sent to\nthe wrong chain the transaction is not misdelivered, it is rejected — after\nspending gas. Solana carries `ProgramID` in the voucher for the same reason.\n\n⚠ **Which chain family a reward pays on is the server's answer, not a guess.** Publishers name\ntheir own networks, so `NetworkID` alone tells you nothing; the creator-facing programme view\ncarries the family explicitly. Guessing here means offering the wrong wallet.\n\n⚠ **`NetAmountNative` is what reaches the wallet**, `AmountNative` is what left\nthe pool. Commission and burn are the difference; show the one you mean.\n\n### `checkJoinEligibility()`\n\nEvaluates the publisher's join requirements against the signed-in player and, if they hold, joins\nthe programme.\n\n```ts\nconst res = await client.communityMarketing.checkJoinEligibility();\nif (!isOk(res)) return;\n\nconst { Passed, Approved, Conditions, WalletMissing } = res.data;\n```\n\n⚠ Only meaningful while the programme's participation mode is `Automatic` and the player has\nalready applied — the application itself is created in the creator portal. In the by-application\nmode the check still reports the breakdown but never admits anyone.\n\n⚠⚠ **It never overrides a human.** A creator the publisher rejected or suspended stays that way\neven with every condition satisfied — `Passed: true, Approved: false` is exactly that case, and the\nUI should say so rather than showing a silent no-op.\n\n⚠ **Render `Conditions`, including on success.** The programme pays for work by rules the person\nnever saw; a bare \"not allowed\" reads as arbitrary, while \"1000 GOLD needed, you have 400\" reads as\na rule they can meet. Showing it on success too tells them what they would lose by dropping below.\n\n⚠ **`Actual: null` means NOT MEASURED, not zero** — the condition was skipped because the outcome\nwas already decided, or the wallet balance could not be read. Rendering it as zero tells the player\nsomething false about their own balance.\n\n## Events\n\n```ts\nclient.on(\"communityMarketing:stateLoaded\", (state) => {});\nclient.on(\"communityMarketing:earningsLoaded\", (earnings) => {});\nclient.on(\"communityMarketing:claimed\", (claim) => {});\nclient.on(\"communityMarketing:withdrawn\", (voucher) => {});\nclient.on(\"communityMarketing:eligibilityChecked\", (verdict) => {});\n```\n\n⚠ `communityMarketing:withdrawn` fires when the **voucher is issued** — the pool\nis already debited, but nothing has reached the chain yet. It means \"payout\nstarted\", not \"money received\".\n\n⚠ `communityMarketing:claimed` fires on **any** successful claim — including\none that paid zero and one that was an idempotent replay. Check `Claimed` and\n`IdempotentReplay` before playing a reward animation.\n\n## A minimal screen\n\n```tsx\nconst [state, setState] = useState<CommunityMarketingState | null>(null);\nconst [earnings, setEarnings] = useState<CommunityEarningsResponse | null>(\n null,\n);\n\nuseEffect(() => {\n void (async () => {\n const s = await client.communityMarketing.getState();\n if (!isOk(s) || !s.data.IsEnabled) return;\n setState(s.data);\n\n // Earnings only make sense for someone actually in the programme.\n if (s.data.IsCreator) {\n const e = await client.communityMarketing.getEarnings();\n if (isOk(e)) setEarnings(e.data);\n }\n })();\n}, []);\n```\n\n- Not enabled → render nothing. A programme the publisher never set up should\n not leave a dead menu entry.\n- Enabled, not a creator → one button: open `CreatorPortalUrl`.\n- Creator → the four numbers (`Earned` / `Mature` / `Granted` / `Paid`), the\n breakdown, and per campaign either a Claim button (game currency) or a\n Withdraw-to-wallet button (crypto).\n\n## Do not\n\n- **Do not poll.** Metrics are sampled by a backend job on a slow schedule\n (hours, not seconds). Refresh on screen open and after a claim; a polling\n loop burns the publisher's API quota to re-read a number that did not move.\n- **Do not build `CreatorPortalUrl`.** It is a server contract; a locally\n assembled address silently 404s the day the route changes.\n- **Do not treat a rejected claim as a client bug.** Fund exhausted, hold\n period, per-creator cap, suspended participation — all of these are the\n backend enforcing the publisher's rules. Surface the message.\n- **Do not implement submission, moderation or metric reading.** They belong to\n the portal and the management backend, and a second implementation would be a\n second place able to create value.\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-getting-started",
|
|
3
3
|
"description": "Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as the project grows). If pulling modules over MCP, use the @idosgames/mcp tools get_host_scaffold / get_module / get_manifest.",
|
|
4
|
-
"content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in\n + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and\n the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever\n a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a\n project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or\n src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and\n idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as\n the project grows). If pulling modules over MCP, use the @idosgames/mcp\n tools get_host_scaffold / get_module / get_manifest.\n---\n\n# Getting started (iDosGames composable modules)\n\nAn iDosGames project is **one host shell + N feature modules**. The host owns everything that exists\nonce — the SDK client, login, the React root, the screen, a module registry, and a Mode Router that\nswitches between modules. A module is a library (a game or app) that plugs into the host; it never\ncreates the client, logs in, or owns the root.\n\n## Runtime packages (npm)\n\n- `@idosgames/core` — the SDK client (`createIDosGamesClient`): auth, currency, store, characters,\n blockchain, ~28 services. See the per-service skills (currency-system, store-system, …).\n- `@idosgames/module-sdk` — the module contract types (`Module`, `ModuleContext`, `EngineScene`,\n `UiPanel`). See idosgames-module-contract.\n- `@idosgames/react` — shared React glue (`IDosGamesProvider`, `useIDosGamesClient`, `useUserState`,\n `StatusProvider`, `createControllerContext`).\n- `@idosgames/app-shell` — the host runtime (`mountHost`, the Mode Router, the module registry).\n- `@idosgames/wallet` — optional wallet bridge (EVM/Solana) for on-chain deposits/withdrawals.\n\nInstall the exact versions from `get_manifest` (MCP) or the registry `index.json` `runtimePackages`.\n\n## Project shape\n\n```\nIDOS.md # the project guide every agent reads first (short, evergreen)\nAGENTS.md, CLAUDE.md # pointers to IDOS.md for external tools\ndocs/feature-history/ # one file per game system + README.md index (the project's memory)\nindex.html # mounts #app\nsrc/main.tsx # creates ONE client and calls mountHost({container, client, modules})\nsrc/modules.ts # the registry: export const modules: Module[] = [ … ] — imports + array only\nsrc/modules/{id}/ # each module's source + its module.meta.json (from get_module)\n```\n\nPlatform-created projects also carry `idos.modules.lock.json` (which modules came from the catalog,\nwith file fingerprints) — it is platform-owned; don't edit it. Where new code goes as the game grows,\nand how the project documents itself, is **idosgames-project-structure**.\n\n`src/main.tsx` (host-owned) is the only place that creates the client — but it does **not** sign in.\n`mountHost` owns sign-in: it replays a previous session (`autoLogin`) and renders the login screen\nwhen there is nothing to replay. Calling a `login*` method here skips that screen for good, taking\n\"switch account\" and wallet sign-in with it:\n\n```ts\nconst client = createIDosGamesClient({ titleID, buildKey, throttleMs: 0 });\n// Do NOT log in here — the host does it.\nmountHost({\n container: app,\n client,\n modules, // from ./modules\n renderLogin, // optional: your own screen; omitted = a plain guest-only default\n});\n```\n\nWhether a returning player is signed back in silently is the login screen's business, not the\nhost's: it calls `client.auth.setRememberSession(remember)` before a `login*` method. See the\nauthentication skill.\n\n`src/modules.ts` registers what the project composes:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nexport const modules: Module[] = [boardGameModule];\n```\n\n## Steps to scaffold\n\n1. **Base** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/base`): sign-in and the lobby in `src/base/`, `src/app.config.ts` (start in the lobby or\n a game), an empty `src/modules.ts` → an empty lobby.\n2. **Bind the Title** — open `src/idos.title.ts` and set `IDOS_TITLE_ID` to the game's canonical\n Title id (and `IDOS_BUILD_KEY` if the title enforces one). This file is the project's single\n centralized identity — it is the highest-priority source and the only channel a packaged mobile\n build, iframe embed, or shared link has. On platform-created projects the platform generates it;\n on a manually scaffolded project **you fill it yourself**. Do NOT bind the title via `.env.local`\n (`VITE_IDOS_TITLE_ID`) — that is a local-dev fallback for the raw template only, and it does not\n travel with the code.\n3. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`. The base's systems are the modules with `inBase: true` (profile,\n inventory, invite, friends, quests, mailbox); add the game(s) with the modules each game `uses`, and any\n other system (shop, VIP, leaderboards…) only when the app needs it.\n4. **Register** — for each module add `import { {camelCase(id)}Module } from \"./modules/{id}\"` and\n push it into the `modules` array (e.g. `board-game` → `boardGameModule`). Then add the module to\n the \"Installed modules\" block of `IDOS.md` (one line: ``- `board-game` (0.1.0) — src/modules/board-game/``)\n — on platform projects the platform maintains that block itself.\n5. **Install deps** — the union of `runtimePackages` + each module's `dependencies`. Modules bring\n their own engine (three, phaser); the host brings react/react-dom/app-shell.\n6. **Run** — sign-in → the lobby: the systems' screens are its tabs (a tab hides itself while the\n title has nothing for it), every game gets a \"Play\" button, and a small button over a game returns\n to the lobby. The lobby draws the balances and the wallet; games hide their own copies by\n themselves (`sharedUi` roles).\n\nTo combine multiple genres into one game, see **idosgames-compose-modules**. To write or edit a\nmodule, see **idosgames-module-contract**.\n\n## Where state lives (decide this before writing the first save)\n\nThe project is client-side code in the player's browser. `localStorage`, module fields, and React\nstate are **not storage** — nothing there survives a device change, and nothing there is trusted.\n\n1. **A dedicated module owns it?** Use that module. Currencies, inventory, quests, characters,\n leaderboards, store purchases each have a service that enforces the rules server-side.\n2. **Otherwise, per-player data → `client.userCustomData`** — buckets `Private`/`Public` are\n client-writable (settings, cosmetics), `ReadOnly`/`Internal` are server-only. Anything a player\n could cheat by editing goes in the server-only buckets. See **user-custom-data**.\n3. **Shared by all players → `client.titleCustomData`** (event state, global counters, server\n thresholds, feature toggles). Read-only for clients. See **title-custom-data**.\n4. **Writing any of the server-only data, or any rule the player must not be able to fake** →\n a CloudCode handler, called with `client.cloudCode.execute(...)`. See **cloud-code**.\n\n## Two MCP surfaces: game CODE vs. a Title's live DATA\n\nThe platform exposes **two independent MCP servers** — don't confuse them:\n\n- **`@idosgames/mcp`** (this one) serves the **CODE registry**. Use it to WRITE a game's source:\n `get_host_scaffold`, `list_modules` / `search` / `get_module`, `get_manifest`, `list_skills` /\n `get_skill`. Transport: stdio (`npx -y @idosgames/mcp`). Its registry is bundled offline; to use the\n hosted copy set `IDOSGAMES_REGISTRY_URL=https://cloud.idosgames.com/drive/registry/latest` — a **base**\n the loader appends `/index.json`, `/modules/{id}.json`, etc. to (the base itself is not fetchable on\n R2; open `.../latest/index.json` to browse the catalog). Read-only, no auth. It never reads or changes\n a live Title's data.\n- **The Title-configuration MCP** (a separate backend server) owns a **live Title's DATA**. Use it to\n read/write the Title's `TitlePublicConfiguration` (`get_<field>` / `save_<field>`, or the whole\n model) and to generate assets (`generate_image` / `generate_audio` / `generate_text` /\n `generate_three_d` / `generate_video`). Transport: HTTP JSON-RPC at\n `POST https://site.idosgames.com/api/v2/mcp`; every tool call takes a `title_id` argument.\n Authorization is **OAuth 2.1** — there is no API key and nothing to paste. Connect it as a plain\n HTTP MCP server with **no headers**: your client gets a `401`, discovers the authorization\n server, registers itself, and opens a browser where the publisher picks which Titles and which\n permissions to grant. The token lives in your client's own credential store, so committed config\n holds only the URL:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\"\n }\n }\n }\n ```\n\n Permissions the publisher can grant: `config:read`, `config:write`, `cloudcode:write`,\n `ai:generate`. A grant is scoped to the Titles ticked on the consent screen, and the publisher\n can revoke it any time from **Connected apps** in the dashboard. If a call comes back\n `SCOPE_NOT_ALLOWED` or `TITLE_NOT_ALLOWED`, the token is fine — that permission or that Title\n simply was not granted; ask the publisher to re-authorize rather than retrying.\n\n Configuring a **fresh (empty) Title** so a game can actually run against it (currencies → game\n loop → bots) is its own checklist: see **idosgames-title-bootstrap**.\n\nRule of thumb: **game CODE → `@idosgames/mcp`; a Title's live config DATA and generated ASSETS → the\nbackend `v2/mcp` server.** Scaffolding a project and configuring/populating the Title it runs as are\ntwo different jobs on two different servers — connect the one that matches the task (or both).\n",
|
|
4
|
+
"content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in\n + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and\n the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever\n a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a\n project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or\n src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and\n idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as\n the project grows). If pulling modules over MCP, use the @idosgames/mcp\n tools get_host_scaffold / get_module / get_manifest.\n---\n\n# Getting started (iDosGames composable modules)\n\nAn iDosGames project is **one host shell + N feature modules**. The host owns everything that exists\nonce — the SDK client, login, the React root, the screen, a module registry, and a Mode Router that\nswitches between modules. A module is a library (a game or app) that plugs into the host; it never\ncreates the client, logs in, or owns the root.\n\n## Runtime packages (npm)\n\n- `@idosgames/core` — the SDK client (`createIDosGamesClient`): auth, currency, store, characters,\n blockchain, ~28 services. See the per-service skills (currency-system, store-system, …). Its\n **central cache** `client.cache` decides every read: the login brings the title config (by version)\n and the player's state (by each container's `Rev`) in ONE call; afterwards read state through\n `client.cache.ensureState([...containers])`, definitions through `getDefinitions()` (served from the\n config cache), server views through their service (cached by policy) — never re-fetch on start-up.\n The server views the game reads ride on that same login call (`ClientState.Views`): the client\n option `loginViews` — `\"auto\"` (default, what the game read last time), `\"all\"`, `\"none\"` or a list.\n- `@idosgames/module-sdk` — the module contract types (`Module`, `ModuleContext`, `EngineScene`,\n `UiPanel`). See idosgames-module-contract.\n- `@idosgames/react` — shared React glue (`IDosGamesProvider`, `useIDosGamesClient`, `useUserState`,\n `StatusProvider`, `createControllerContext`).\n- `@idosgames/app-shell` — the host runtime (`mountHost`, the Mode Router, the module registry).\n- `@idosgames/wallet` — optional wallet bridge (EVM/Solana) for on-chain deposits/withdrawals.\n\nInstall the exact versions from `get_manifest` (MCP) or the registry `index.json` `runtimePackages`.\n\n## Project shape\n\n```\nIDOS.md # the project guide every agent reads first (short, evergreen)\nAGENTS.md, CLAUDE.md # pointers to IDOS.md for external tools\ndocs/feature-history/ # one file per game system + README.md index (the project's memory)\nindex.html # mounts #app\nsrc/main.tsx # creates ONE client and calls mountHost({container, client, modules})\nsrc/modules.ts # the registry: export const modules: Module[] = [ … ] — imports + array only\nsrc/modules/{id}/ # each module's source + its module.meta.json (from get_module)\n```\n\nPlatform-created projects also carry `idos.modules.lock.json` (which modules came from the catalog,\nwith file fingerprints) — it is platform-owned; don't edit it. Where new code goes as the game grows,\nand how the project documents itself, is **idosgames-project-structure**.\n\n`src/main.tsx` (host-owned) is the only place that creates the client — but it does **not** sign in.\n`mountHost` owns sign-in: it replays a previous session (`autoLogin`) and renders the login screen\nwhen there is nothing to replay. Calling a `login*` method here skips that screen for good, taking\n\"switch account\" and wallet sign-in with it:\n\n```ts\nconst client = createIDosGamesClient({ titleID, buildKey, throttleMs: 0 });\n// Do NOT log in here — the host does it.\nmountHost({\n container: app,\n client,\n modules, // from ./modules\n renderLogin, // optional: your own screen; omitted = a plain guest-only default\n});\n```\n\nWhether a returning player is signed back in silently is the login screen's business, not the\nhost's: it calls `client.auth.setRememberSession(remember)` before a `login*` method. See the\nauthentication skill.\n\n`src/modules.ts` registers what the project composes:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nexport const modules: Module[] = [boardGameModule];\n```\n\n## Steps to scaffold\n\n1. **Base** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/base`): sign-in and the lobby in `src/base/`, `src/app.config.ts` (start in the lobby or\n a game), an empty `src/modules.ts` → an empty lobby.\n2. **Bind the Title** — open `src/idos.title.ts` and set `IDOS_TITLE_ID` to the game's canonical\n Title id (and `IDOS_BUILD_KEY` if the title enforces one). This file is the project's single\n centralized identity — it is the highest-priority source and the only channel a packaged mobile\n build, iframe embed, or shared link has. On platform-created projects the platform generates it;\n on a manually scaffolded project **you fill it yourself**. Do NOT bind the title via `.env.local`\n (`VITE_IDOS_TITLE_ID`) — that is a local-dev fallback for the raw template only, and it does not\n travel with the code.\n3. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`. The base's systems are the modules with `inBase: true` (profile,\n inventory, invite, friends, quests, mailbox); add the game(s) with the modules each game `uses`, and any\n other system (shop, VIP, leaderboards…) only when the app needs it.\n4. **Register** — for each module add `import { {camelCase(id)}Module } from \"./modules/{id}\"` and\n push it into the `modules` array (e.g. `board-game` → `boardGameModule`). Then add the module to\n the \"Installed modules\" block of `IDOS.md` (one line: ``- `board-game` (0.1.0) — src/modules/board-game/``)\n — on platform projects the platform maintains that block itself.\n5. **Install deps** — the union of `runtimePackages` + each module's `dependencies`. Modules bring\n their own engine (three, phaser); the host brings react/react-dom/app-shell.\n6. **Run** — sign-in → the lobby: the systems' screens are its tabs (a tab hides itself while the\n title has nothing for it), every game gets a \"Play\" button, and a small button over a game returns\n to the lobby. The lobby draws the balances and the wallet; games hide their own copies by\n themselves (`sharedUi` roles).\n\nTo combine multiple genres into one game, see **idosgames-compose-modules**. To write or edit a\nmodule, see **idosgames-module-contract**.\n\n## Where state lives (decide this before writing the first save)\n\nThe project is client-side code in the player's browser. `localStorage`, module fields, and React\nstate are **not storage** — nothing there survives a device change, and nothing there is trusted.\n\n1. **A dedicated module owns it?** Use that module. Currencies, inventory, quests, characters,\n leaderboards, store purchases each have a service that enforces the rules server-side.\n2. **Otherwise, per-player data → `client.userCustomData`** — buckets `Private`/`Public` are\n client-writable (settings, cosmetics), `ReadOnly`/`Internal` are server-only. Anything a player\n could cheat by editing goes in the server-only buckets. See **user-custom-data**.\n3. **Shared by all players → `client.titleCustomData`** (event state, global counters, server\n thresholds, feature toggles). Read-only for clients. See **title-custom-data**.\n4. **Writing any of the server-only data, or any rule the player must not be able to fake** →\n a CloudCode handler, called with `client.cloudCode.execute(...)`. See **cloud-code**.\n\n## Two MCP surfaces: game CODE vs. a Title's live DATA\n\nThe platform exposes **two independent MCP servers** — don't confuse them:\n\n- **`@idosgames/mcp`** (this one) serves the **CODE registry**. Use it to WRITE a game's source:\n `get_host_scaffold`, `list_modules` / `search` / `get_module`, `get_manifest`, `list_skills` /\n `get_skill`. Transport: stdio (`npx -y @idosgames/mcp`). Its registry is bundled offline; to use the\n hosted copy set `IDOSGAMES_REGISTRY_URL=https://cloud.idosgames.com/drive/registry/latest` — a **base**\n the loader appends `/index.json`, `/modules/{id}.json`, etc. to (the base itself is not fetchable on\n R2; open `.../latest/index.json` to browse the catalog). Read-only, no auth. It never reads or changes\n a live Title's data.\n- **The Title-configuration MCP** (a separate backend server) owns a **live Title's DATA**. Use it to\n read/write the Title's `TitlePublicConfiguration` (`get_<field>` / `save_<field>`, or the whole\n model) and to generate assets (`generate_image` / `generate_audio` / `generate_text` /\n `generate_three_d` / `generate_video`). Transport: HTTP JSON-RPC at\n `POST https://site.idosgames.com/api/v2/mcp`; every tool call takes a `title_id` argument.\n Authorization is **OAuth 2.1** — there is no API key and nothing to paste. Connect it as a plain\n HTTP MCP server with **no headers**: your client gets a `401`, discovers the authorization\n server, registers itself, and opens a browser where the publisher picks which Titles and which\n permissions to grant. The token lives in your client's own credential store, so committed config\n holds only the URL:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\"\n }\n }\n }\n ```\n\n Permissions the publisher can grant: `config:read`, `config:write`, `cloudcode:write`,\n `ai:generate`. A grant is scoped to the Titles ticked on the consent screen, and the publisher\n can revoke it any time from **Connected apps** in the dashboard. If a call comes back\n `SCOPE_NOT_ALLOWED` or `TITLE_NOT_ALLOWED`, the token is fine — that permission or that Title\n simply was not granted; ask the publisher to re-authorize rather than retrying.\n\n Configuring a **fresh (empty) Title** so a game can actually run against it (currencies → game\n loop → bots) is its own checklist: see **idosgames-title-bootstrap**.\n\nRule of thumb: **game CODE → `@idosgames/mcp`; a Title's live config DATA and generated ASSETS → the\nbackend `v2/mcp` server.** Scaffolding a project and configuring/populating the Title it runs as are\ntwo different jobs on two different servers — connect the one that matches the task (or both).\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-ui-kit",
|
|
3
3
|
"description": "Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts), install a shop / VIP / heroes / inventory / chests / boosts / leaderboard / daily rewards / quests / collections / craft / marketplace / offers / events / team event / season / friends / chat / invite / arena / multiplayer rooms / profile / tutorials / workshop / AI studio system instead of writing it, open another module's screen with ctx.features.open, register a new screen (a lobby tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this whenever the user asks to change how the game's interface looks, sounds or animates, to add or build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features, FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.",
|
|
4
|
-
"content": "---\nname: idosgames-ui-kit\ndescription: >-\n Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready\n system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts),\n install a shop / VIP / heroes / inventory / chests / boosts / leaderboard / daily rewards / quests /\n collections / craft / marketplace / offers / events / team event / season / friends / chat / invite /\n arena / multiplayer rooms / profile / tutorials / workshop / AI studio system instead of writing it, open another module's screen with ctx.features.open, register a new screen (a lobby\n tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a\n phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for\n tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this\n whenever the user asks to change how the game's interface looks, sounds or animates, to add or\n build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a\n settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features,\n FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.\n---\n\n# The UI kit and the system modules\n\nThe app's interface is the base's lobby (`src/base/`) plus **system modules**, all on one **UI kit**:\n\n- `@idosgames/react/ui` — the kit: components (buttons, tabs, popups, resource lists, item tiles,\n progress bars, timers), the reward popup, toasts, UI sounds (synthesized, no files), animations,\n and the player's own sound / motion prefs.\n- system modules — the platform's systems, each a whole `feature` module (its screen = a lobby\n tab, its client logic, its demo settings). The base installs six of them into every new project\n (`inBase`: profile, inventory, invite, friends, quests, mailbox); the rest are installed only when the app\n needs them (a game's `uses`, or a task that needs what they `provide`). ONE system per player-facing module of the engine\n (`API/Client/v2/<Module>`) — the owner's rule since 29.09.2026. The module id is the feature id:\n\n| Module | Engine module | Screen |\n| ------------- | -------------- | ------------------------------------------------------------- |\n| `store` | Store+Purchase | shop: stores as tabs, offers, purchase popup |\n| `premium` | Premium | VIP passes: benefits, timer, free trial, buy / extend |\n| `character` | Character | heroes: gear slots, \"Equip best\", stats, ranks, skins |\n| `inventory` | Item | items by rarity, item card (always shown, even with no items) |\n| `lootboxes` | Lootbox | chests: every way to open, ×1 / ×10, opening animation |\n| `boosts` | TimedBoost | running boosts + global happy hours, activate a boost |\n| `leaderboard` | Leaderboard | tables, my place, rank rewards, last cycle's prize |\n| `rewards` | Reward | daily login calendar, greeting popup |\n| `quests` | Quest | quests by cycle, details, claim all, points track |\n| `collections` | Collection | album of sets, packs, chests for points, card trades |\n| `craft` | Craft | recipes, contract popup |\n| `marketplace` | Marketplace | lots, auctions, selling, trades |\n| `deals` | DealOffer | special offers, auto-popup of a new deal |\n| `events` | TimedEvent | limited-time events, milestones, VIP prize |\n| `coop-events` | CoopEvent | team event: join a group, spin to build, team prize |\n| `season` | Season | season tiers, VIP prize |\n| `friends` | Social | friends, requests, suggestions, news |\n| `chat` | Chat | channels, private chats |\n| `invite` | Referral | own code and link, friend's code, rewards (always shown) |\n| `pvp` | Match | duels, strategy, fight replay |\n| `multiplayer` | Multiplayer | rooms: list, create / join by code, ready, chat, host \"Play\" |\n| `profile` | User | name, sound, language, consent, delete account |\n| `tutorials` | Tutorial | hint overlay + replay list |\n| `workshop` | Workshop | players' and the publisher's content: catalog, publish |\n| `ai` | AI | AI studio: the title's AI features for players, history |\n\nNot a system on purpose: Currency, Authentication, Blockchain (the base — balances, sign-in,\nwallet); GameLoop (the `board-game` game); CommunityMarketing (idosgames.com only); Mailbox and\nAdvertising (no client handler / no web ad network); Analytics, Experiment, Push, Localization,\nCloudCode, DataCollections, TitleCustomData, UserCustomData (services the systems and games call).\n\nHand-offs between systems go through `ctx.features.open(id, args)`: inventory → `character`\n(`{ itemInstanceID }`) and → `lootboxes` (`{ itemID }` of a chest or key); events / season →\n`premium` (`{ premiumID }`). Multiplayer: the `multiplayer` system owns the rooms; a game registers\nin `ctx.multiplayer` and takes the room in `start(room)` (skill **multiplayer-realtime**); the lobby\nshows \"With friends\" next to such a game's \"Play\".\n\n**Need one of these? Install the module — don't write the screen.** Every screen follows the\nserver's rules and ships demo title settings (`title-seed.json`). The app does not need one → remove it\nfrom `src/modules.ts`, its tab is gone. The lobby itself (tabs, balances, wallet, profile, \"Play\") is\nthe base, `src/base/lobby/`.\n\n## Change the look, animations and sounds — only in `src/ui.config.ts`\n\n```ts\nimport { defineUiConfig } from \"@idosgames/react/ui\";\n\nexport default defineUiConfig({\n theme: {\n bgTop: \"#10263f\",\n panel: \"#1b3a5c\",\n gold: \"#ffcc33\",\n radius: 12,\n rarity: { Mythic: \"#ff3df5\" },\n },\n motion: { speed: 1.5, intensity: 0.5, particles: false },\n sound: {\n volume: 0.4,\n events: {\n click: \"tab\",\n countTick: false,\n reward: { url: \"https://…/win.mp3\" },\n },\n },\n});\n```\n\n- Every field is optional; what you leave out keeps its default. The host passes the file to\n `mountHost({ ui })`, and every module on the kit follows it at once.\n- ⚠ **Never restyle a catalog module by editing it.** An edited module is marked \"customized\" and\n stops getting catalog updates. Colours, radii, fonts, animation speed and sounds are all here.\n- **One theme for the WHOLE app:** sign-in (`src/base/login/`), the lobby, every system and the host's\n own screens all draw from these tokens. A new look — even a light theme — is an edit of this file\n ONLY: never recolour `LoginScreen.tsx`, the logo, icons or `index.html` by hand (the logo and the\n sign-in icons are painted with the theme's colours, the page background follows `bgBottom`).\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `redDeep`, `shadow`, `backdrop`,\n `radius`, `buttonRadius`, `font`, `rarity` (rarity id → frame colour).\n- **Derived tokens — usually leave them out.** They are computed from the colours above: set a light\n `panel` and dark `text`, and the dark text outline disappears (`textOutline`), lines and recesses\n turn faint-dark instead of white/black (`line`, `well`, `wellSoft`, `wellShadow`, `edge`), cards get a\n soft shadow (`panelShadow`), and buttons of a pastel tone get dark text (`onGreen`, `onGold`,\n `onBlue`, `onRed`). Set one only to override the computed value.\n- A light theme is just colours:\n `theme: { bgTop: \"#f4fcf8\", bgBottom: \"#d9f0e6\", panel: \"#ffffff\", panelDeep: \"#f1faf5\",\npanelEdge: \"#bfe6d3\", text: \"#1f3b30\", textDim: \"#5f7d71\", green: \"#7ee0b5\", greenDeep: \"#4cc493\" }`.\n- Motion: `enabled`, `speed` (2 = twice as fast), `intensity` (0 = no bounce), `rewardFlight`,\n `particles`, `stagger`, `respectReducedMotion`.\n- Sound events: `click`, `tab`, `open`, `close`, `reward`, `coin`, `purchase`, `claim`, `error`,\n `notify`, `lootbox`, `levelUp`, `countTick`. Each: another event's name, `{ url }`, `{ synth: {\nnotes: [{ freq, dur, wave }] } }` or `false`.\n- The PLAYER's own sound / volume / \"fewer animations\" live on the device (the profile module draws\n them):\n `const [prefs, setPrefs] = usePlayerPrefs()` → `{ muted, volume, reducedMotion }`.\n\n## Features: screens that find each other\n\nA module registers a **feature**; whoever draws the menu shows it — the base's lobby as tabs (phone:\n4 + \"More\", tablet: more, desktop: a side rail; the base takes the shared-UI role `\"menu\"`), or the\nhost's floating ☰ when nobody takes the role. Tab order and main tabs can be overridden in\n`src/app.config.ts` (`lobby.order`, `lobby.primary`).\n\n```ts\n// module.ts\nsetup(ctx) {\n ctx.features.register({\n id: \"forge\", label: \"Forge\", icon: \"hammer\", group: \"economy\", order: 60,\n primary: false, // true = a main tab of the lobby\n Screen: ForgeScreen, // (props: FeatureScreenProps) => ReactNode; props.args, props.close()\n available: false, // hidden until the title has something for it — see below\n });\n // Badge (\"something to claim\"): ctx.features.setBadge(\"forge\", 2)\n // The title has forge recipes → show the tab; the reason is for the AI Coder's agent only:\n ctx.features.setAvailable(\"forge\", hasRecipes(configSection(ctx.client, \"Craft\")), \"no forge recipes\");\n}\n```\n\n- Open another module's screen without importing it: `ctx.features.open(\"store\")`,\n `ctx.features.open(\"chat\", { userID })`. It returns `false` when the module is not installed or its\n tab is hidden (`ctx.features.get(\"store\")?.available`). From a game (the lobby is off screen) the\n host shows the screen in a window.\n- A screen that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows — from the client config\n (`configSection(client, \"Store\")`) or a read it makes anyway (a badge watcher); no extra request\n at start-up: every module's first reads arrive together and brush the per-player rate limit.\n- Groups: `economy`, `events`, `social`, `profile`. Icons are kit glyph names (`shop`, `hero`,\n `bag`, `trophy`, `gift`, `hammer`, `market`, `offer`, `flame`, `friends`, `chat`, `link`,\n `swords`, `user`, `bulb`, …) or an image URL.\n- Something that must be on screen all the time (a daily auto-popup, a badge watcher, an overlay) is\n a panel: `ctx.registerPanel({ id, slot: \"modal\", activeOnly: false, component })`.\n\n## Building a screen on the kit\n\nKeep the screen a short list; put every detail in a **popup**.\n\n```tsx\nimport {\n Button,\n Popup,\n ResourceList,\n Tabs,\n useCelebrate,\n useToast,\n useUiKit,\n errorText,\n grantedBy,\n panel,\n outlined,\n v,\n} from \"@idosgames/react/ui\";\n\nfunction ForgeScreen(): ReactNode {\n const [open, setOpen] = useState<string | null>(null);\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n {/* rows… onClick={() => setOpen(id)} */}\n {open ? (\n <Popup title=\"Forge\" onClose={() => setOpen(null)} width={420}>\n …\n </Popup>\n ) : null}\n </div>\n );\n}\n```\n\n- `Popup` is a bottom sheet on a phone and a centred window elsewhere (`variant=\"center\"` forces\n centred); it animates in and out and plays open/close sounds. Close from inside with\n `usePopupClose()`. `ConfirmPopup` for yes/no. A popup that opens BY ITSELF (not on a tap) passes\n `queued` — auto-popups then show one at a time instead of stacking.\n- `useCelebrate()(lines)` shows \"You got\" and flies the icons to any element marked\n `data-resource-counter=\"{currencyID}\"` (the lobby's balances are). Build the lines\n from a response with `grantedBy(data)`; a reward with premium (VIP) bundles needs them added.\n- `useUiKit().play(\"claim\")`, `useUiKit().fx.burst(element)` — a sound and a particle burst.\n- **Notifications — `useNotify()`** (the same system as a module's `ctx.notifications`):\n `notify.toast({ level: \"success\", title: \"Saved\" })` — a toast at the top that goes by itself;\n `await notify.alert({ level: \"warning\", title, message, actions })` — a window the player must\n answer, queued one at a time. Levels `info | success | warning | error | reward` pick the colour,\n icon, sound and how long a toast stays (errors longest). `icon` (a glyph, emoji or URL), `image` (a\n thumbnail / a big picture in the window), `source` (a feature id — its name on the card, a tap opens\n it), `key` (the same key replaces instead of stacking; identical toasts merge into \"×2\"),\n `duration` (`0` — until closed), `actions`. Refusals: `notify.toast({ level: \"error\", message:\nerrorText(res.error) })`; never re-check server rules. `useToast()(text, kind)` is the old short\n form of the same thing.\n- Lines of resources: `grantLines(grant)`, `costLines(cost)`, `bundleLines(bundle)`,\n `canAfford(lines, balances)`, `<ResourceList lines={…} />`. Names and icons: `useCatalog()`.\n- Words: `makeT(EN, RU)` for your strings, `plural(n, \"лот|лота|лотов\")` for counts.\n- ⚠ A root `display: grid` with a wide child (a row of tabs) grows past a phone screen — give it\n `gridTemplateColumns: \"minmax(0, 1fr)\"`.\n- ⚠ **No colour literals in a screen** (`\"#fff\"`, `\"rgba(0,0,0,.3)\"`, `\"#5a3fd0\"`): they break the\n moment the theme changes (white text on a light panel, a black stripe under a pastel button). Use\n the tokens: text `v.text` / `v.textDim` (or `...outlined`), surfaces `panel` / `well` / `v.wellSoft`,\n lines `v.line`, dimming `v.backdrop`. A coloured banner, \"your row\", a highlighted day →\n `...toneSurface(\"blue\" | \"green\" | \"gold\" | \"red\")`: it fills with that tone AND switches the text\n inside it to the tone's own readable colour. Text on a filled button — `ON_TONE[tone]`. A blend:\n `` `color-mix(in srgb, ${v.gold} 20%, transparent)` ``.\n\n## Tutorial hints\n\nThe `tutorials` module highlights any element marked `data-tutorial-anchor=\"…\"` — the lobby's tabs\nare `tab:{featureID}`, its profile button `lobby:profile`, its wallet `lobby:wallet`, a game's \"Play\"\nbutton `play:{modeID}`. Mark the controls of your own screen the same\nway; the publisher points tutorial steps at them in LiveOps (Tutorial → step → AnchorID).\n",
|
|
4
|
+
"content": "---\nname: idosgames-ui-kit\ndescription: >-\n Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready\n system modules (store, character, inventory, …): change colours, animations and sounds in ONE file (src/ui.config.ts),\n install a shop / VIP / heroes / inventory / chests / boosts / leaderboard / daily rewards / quests /\n collections / craft / marketplace / offers / events / team event / season / friends / chat / invite /\n arena / multiplayer rooms / profile / tutorials / workshop / AI studio system instead of writing it, open another module's screen with ctx.features.open, register a new screen (a lobby\n tab) with ctx.features.register and hide it while the title has no data (setAvailable), split a dense screen into popups (Popup, ConfirmPopup, bottom sheet on a\n phone), play UI sounds, celebrate a reward (icons fly to the balance counters), mark elements for\n tutorial hints (data-tutorial-anchor), and read the player's sound / motion prefs. Use this\n whenever the user asks to change how the game's interface looks, sounds or animates, to add or\n build a game screen or menu, a popup, a reward animation, a bottom navigation, a \"More\" menu, a\n settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features,\n FeatureScreenProps, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.\n---\n\n# The UI kit and the system modules\n\nThe app's interface is the base's lobby (`src/base/`) plus **system modules**, all on one **UI kit**:\n\n- `@idosgames/react/ui` — the kit: components (buttons, tabs, popups, resource lists, item tiles,\n progress bars, timers), the reward popup, toasts, UI sounds (synthesized, no files), animations,\n and the player's own sound / motion prefs.\n- system modules — the platform's systems, each a whole `feature` module (its screen = a lobby\n tab, its client logic, its demo settings). The base installs six of them into every new project\n (`inBase`: profile, inventory, invite, friends, quests, mailbox); the rest are installed only when the app\n needs them (a game's `uses`, or a task that needs what they `provide`). ONE system per player-facing module of the engine\n (`API/Client/v2/<Module>`) — the owner's rule since 29.09.2026. The module id is the feature id:\n\n| Module | Engine module | Screen |\n| ------------- | -------------- | ------------------------------------------------------------- |\n| `store` | Store+Purchase | shop: stores as tabs, offers, purchase popup |\n| `premium` | Premium | VIP passes: benefits, timer, free trial, buy / extend |\n| `character` | Character | heroes: gear slots, \"Equip best\", stats, ranks, skins |\n| `inventory` | Item | items by rarity, item card (always shown, even with no items) |\n| `lootboxes` | Lootbox | chests: every way to open, ×1 / ×10, opening animation |\n| `boosts` | TimedBoost | running boosts + global happy hours, activate a boost |\n| `leaderboard` | Leaderboard | tables, my place, rank rewards, last cycle's prize |\n| `rewards` | Reward | daily login calendar, greeting popup |\n| `quests` | Quest | quests by cycle, details, claim all, points track |\n| `collections` | Collection | album of sets, packs, chests for points, card trades |\n| `craft` | Craft | recipes, contract popup |\n| `marketplace` | Marketplace | lots, auctions, selling, trades |\n| `deals` | DealOffer | special offers, auto-popup of a new deal |\n| `events` | TimedEvent | limited-time events, milestones, VIP prize |\n| `coop-events` | CoopEvent | team event: join a group, spin to build, team prize |\n| `season` | Season | season tiers, VIP prize |\n| `friends` | Social | friends, requests, suggestions, news |\n| `chat` | Chat | channels, private chats |\n| `invite` | Referral | own code and link, friend's code, rewards (always shown) |\n| `pvp` | Match | duels, strategy, fight replay |\n| `multiplayer` | Multiplayer | rooms: list, create / join by code, ready, chat, host \"Play\" |\n| `profile` | User | name, sound, language, consent, delete account |\n| `tutorials` | Tutorial | hint overlay + replay list |\n| `workshop` | Workshop | players' and the publisher's content: catalog, publish |\n| `ai` | AI | AI studio: the title's AI features for players, history |\n\nNot a system on purpose: Currency, Authentication, Blockchain (the base — balances, sign-in,\nwallet); GameLoop (the `board-game` game); CommunityMarketing (idosgames.com only); Mailbox and\nAdvertising (no client handler / no web ad network); Analytics, Experiment, Push, Localization,\nCloudCode, DataCollections, TitleCustomData, UserCustomData (services the systems and games call).\n\nHand-offs between systems go through `ctx.features.open(id, args)`: inventory → `character`\n(`{ itemInstanceID }`) and → `lootboxes` (`{ itemID }` of a chest or key); events / season →\n`premium` (`{ premiumID }`). Multiplayer: the `multiplayer` system owns the rooms; a game registers\nin `ctx.multiplayer` and takes the room in `start(room)` (skill **multiplayer-realtime**); the lobby\nshows \"With friends\" next to such a game's \"Play\".\n\n**Need one of these? Install the module — don't write the screen.** Every screen follows the\nserver's rules and ships demo title settings (`title-seed.json`). The app does not need one → remove it\nfrom `src/modules.ts`, its tab is gone. The lobby itself (tabs, balances, wallet, profile, \"Play\") is\nthe base, `src/base/lobby/`.\n\n## Change the look, animations and sounds — only in `src/ui.config.ts`\n\n```ts\nimport { defineUiConfig } from \"@idosgames/react/ui\";\n\nexport default defineUiConfig({\n theme: {\n bgTop: \"#10263f\",\n panel: \"#1b3a5c\",\n gold: \"#ffcc33\",\n radius: 12,\n rarity: { Mythic: \"#ff3df5\" },\n },\n motion: { speed: 1.5, intensity: 0.5, particles: false },\n sound: {\n volume: 0.4,\n events: {\n click: \"tab\",\n countTick: false,\n reward: { url: \"https://…/win.mp3\" },\n },\n },\n});\n```\n\n- Every field is optional; what you leave out keeps its default. The host passes the file to\n `mountHost({ ui })`, and every module on the kit follows it at once.\n- ⚠ **Never restyle a catalog module by editing it.** An edited module is marked \"customized\" and\n stops getting catalog updates. Colours, radii, fonts, animation speed and sounds are all here.\n- **One theme for the WHOLE app:** sign-in (`src/base/login/`), the lobby, every system and the host's\n own screens all draw from these tokens. A new look — even a light theme — is an edit of this file\n ONLY: never recolour `LoginScreen.tsx`, the logo, icons or `index.html` by hand (the logo and the\n sign-in icons are painted with the theme's colours, the page background follows `bgBottom`).\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `redDeep`, `shadow`, `backdrop`,\n `radius`, `buttonRadius`, `font`, `rarity` (rarity id → frame colour).\n- **Derived tokens — usually leave them out.** They are computed from the colours above: set a light\n `panel` and dark `text`, and the dark text outline disappears (`textOutline`), lines and recesses\n turn faint-dark instead of white/black (`line`, `well`, `wellSoft`, `wellShadow`, `edge`), cards get a\n soft shadow (`panelShadow`), and buttons of a pastel tone get dark text (`onGreen`, `onGold`,\n `onBlue`, `onRed`). Set one only to override the computed value.\n- A light theme is just colours:\n `theme: { bgTop: \"#f4fcf8\", bgBottom: \"#d9f0e6\", panel: \"#ffffff\", panelDeep: \"#f1faf5\",\npanelEdge: \"#bfe6d3\", text: \"#1f3b30\", textDim: \"#5f7d71\", green: \"#7ee0b5\", greenDeep: \"#4cc493\" }`.\n- Motion: `enabled`, `speed` (2 = twice as fast), `intensity` (0 = no bounce), `rewardFlight`,\n `particles`, `stagger`, `respectReducedMotion`.\n- Sound events: `click`, `tab`, `open`, `close`, `reward`, `coin`, `purchase`, `claim`, `error`,\n `notify`, `lootbox`, `levelUp`, `countTick`. Each: another event's name, `{ url }`, `{ synth: {\nnotes: [{ freq, dur, wave }] } }` or `false`.\n- The PLAYER's own sound / volume / \"fewer animations\" live on the device (the profile module draws\n them):\n `const [prefs, setPrefs] = usePlayerPrefs()` → `{ muted, volume, reducedMotion }`.\n\n## Features: screens that find each other\n\nA module registers a **feature**; whoever draws the menu shows it — the base's lobby as tabs (phone:\n4 + \"More\", tablet: more, desktop: a side rail; the base takes the shared-UI role `\"menu\"`), or the\nhost's floating ☰ when nobody takes the role. Tab order and main tabs can be overridden in\n`src/app.config.ts` (`lobby.order`, `lobby.primary`).\n\n```ts\n// module.ts\nsetup(ctx) {\n ctx.features.register({\n id: \"forge\", label: \"Forge\", icon: \"hammer\", group: \"economy\", order: 60,\n primary: false, // true = a main tab of the lobby\n Screen: ForgeScreen, // (props: FeatureScreenProps) => ReactNode; props.args, props.close()\n available: false, // hidden until the title has something for it — see below\n });\n // Badge (\"something to claim\"): ctx.features.setBadge(\"forge\", 2)\n // The title has forge recipes → show the tab; the reason is for the AI Coder's agent only:\n ctx.features.setAvailable(\"forge\", hasRecipes(configSection(ctx.client, \"Craft\")), \"no forge recipes\");\n}\n```\n\n- Open another module's screen without importing it: `ctx.features.open(\"store\")`,\n `ctx.features.open(\"chat\", { userID })`. It returns `false` when the module is not installed or its\n tab is hidden (`ctx.features.get(\"store\")?.available`). From a game (the lobby is off screen) the\n host shows the screen in a window.\n- A screen that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows — from the client config\n (`configSection(client, \"Store\")`) or a read it makes anyway (a badge watcher); no extra request\n at start-up: every module's first reads arrive together and brush the per-player rate limit.\n- Groups: `economy`, `events`, `social`, `profile`. Icons are kit glyph names (`shop`, `hero`,\n `bag`, `trophy`, `gift`, `hammer`, `market`, `offer`, `flame`, `friends`, `chat`, `link`,\n `swords`, `user`, `bulb`, …) or an image URL.\n- Something that must be on screen all the time (a daily auto-popup, a badge watcher, an overlay) is\n a panel: `ctx.registerPanel({ id, slot: \"modal\", activeOnly: false, component })`.\n- **A badge must go out the moment the reward is taken.** Count it in the always-on watcher from the\n CURRENT cache on every change — `client.on(\"user:anyUpdated\", update)` reading\n `client.data.user.state` — not in an effect keyed on a state object (the cache patches it in place,\n so such an effect does not re-run and the badge stays lit). A badge counted from data the screen\n re-reads (a storefront, active events) also listens to that read's event\n (`store:storefrontLoaded`…), and a store that several places reload runs one read at a time and keeps\n what it has when a read fails — see `modules/events` / `modules/deals`.\n- **Never re-read at start-up what the login already brought** — read through the central cache\n (`client.cache`, core ≥ 0.21). State containers: `client.cache.ensureState([\"Quest\"])` (no request\n while the container's `Rev` is confirmed; the server only when it must compute something — a new\n quest cycle, a hero given by default); a screen that wants changes made elsewhere passes\n `{ maxAgeMs: 60_000 }`. Definitions: `client.x.getDefinitions()` answers from the title config cache.\n Server views (storefront, active events/deals/season, unread counters, leaderboard progress) are\n cached by their service — call them freely, and take the data FROM THE ANSWER: the login usually\n brings them before a watcher subscribes, and a read the cache answers emits no `*Loaded` event. A game's own server data: `defineView` + `client.cache.read`.\n- A section the engine does not put into the client config (`TimedBoost`, `Marketplace`, `Match`,\n `AI`) is read with the service's own `getDefinitions()` — it lands in the same config section and is\n kept under the config version, so the next login does not ask again.\n- Auto-popups (`<Popup queued>`) wait while a game mode is on screen and show in the lobby.\n\n## Building a screen on the kit\n\nKeep the screen a short list; put every detail in a **popup**.\n\n```tsx\nimport {\n Button,\n Popup,\n ResourceList,\n Tabs,\n useCelebrate,\n useToast,\n useUiKit,\n errorText,\n grantedBy,\n panel,\n outlined,\n v,\n} from \"@idosgames/react/ui\";\n\nfunction ForgeScreen(): ReactNode {\n const [open, setOpen] = useState<string | null>(null);\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n {/* rows… onClick={() => setOpen(id)} */}\n {open ? (\n <Popup title=\"Forge\" onClose={() => setOpen(null)} width={420}>\n …\n </Popup>\n ) : null}\n </div>\n );\n}\n```\n\n- `Popup` is a bottom sheet on a phone and a centred window elsewhere (`variant=\"center\"` forces\n centred); it animates in and out and plays open/close sounds. Close from inside with\n `usePopupClose()`. `ConfirmPopup` for yes/no. A popup that opens BY ITSELF (not on a tap) passes\n `queued` — auto-popups then show one at a time instead of stacking.\n- `useCelebrate()(lines)` shows \"You got\" and flies the icons to any element marked\n `data-resource-counter=\"{currencyID}\"` (the lobby's balances are). Build the lines\n from a response with `grantedBy(data)`; a reward with premium (VIP) bundles needs them added.\n- `useUiKit().play(\"claim\")`, `useUiKit().fx.burst(element)` — a sound and a particle burst.\n- **Notifications — `useNotify()`** (the same system as a module's `ctx.notifications`):\n `notify.toast({ level: \"success\", title: \"Saved\" })` — a toast at the top that goes by itself;\n `await notify.alert({ level: \"warning\", title, message, actions })` — a window the player must\n answer, queued one at a time. Levels `info | success | warning | error | reward` pick the colour,\n icon, sound and how long a toast stays (errors longest). `icon` (a glyph, emoji or URL), `image` (a\n thumbnail / a big picture in the window), `source` (a feature id — its name on the card, a tap opens\n it), `key` (the same key replaces instead of stacking; identical toasts merge into \"×2\"),\n `duration` (`0` — until closed), `actions`. Refusals: `notify.toast({ level: \"error\", message:\nerrorText(res.error) })`; never re-check server rules. `useToast()(text, kind)` is the old short\n form of the same thing.\n- Lines of resources: `grantLines(grant)`, `costLines(cost)`, `bundleLines(bundle)`,\n `canAfford(lines, balances)`, `<ResourceList lines={…} />`. Names and icons: `useCatalog()`.\n- Words: `makeT(EN, RU)` for your strings, `plural(n, \"лот|лота|лотов\")` for counts.\n- ⚠ A root `display: grid` with a wide child (a row of tabs) grows past a phone screen — give it\n `gridTemplateColumns: \"minmax(0, 1fr)\"`.\n- ⚠ **No colour literals in a screen** (`\"#fff\"`, `\"rgba(0,0,0,.3)\"`, `\"#5a3fd0\"`): they break the\n moment the theme changes (white text on a light panel, a black stripe under a pastel button). Use\n the tokens: text `v.text` / `v.textDim` (or `...outlined`), surfaces `panel` / `well` / `v.wellSoft`,\n lines `v.line`, dimming `v.backdrop`. A coloured banner, \"your row\", a highlighted day →\n `...toneSurface(\"blue\" | \"green\" | \"gold\" | \"red\")`: it fills with that tone AND switches the text\n inside it to the tone's own readable colour. Text on a filled button — `ON_TONE[tone]`. A blend:\n `` `color-mix(in srgb, ${v.gold} 20%, transparent)` ``.\n\n## Tutorial hints\n\nThe `tutorials` module highlights any element marked `data-tutorial-anchor=\"…\"` — the lobby's tabs\nare `tab:{featureID}`, its profile button `lobby:profile`, its wallet `lobby:wallet`, a game's \"Play\"\nbutton `play:{modeID}`. Mark the controls of your own screen the same\nway; the publisher points tutorial steps at them in LiveOps (Tutorial → step → AnchorID).\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailbox-system",
|
|
3
3
|
"description": "Build a mailbox / inbox in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.mailbox (MailboxService): letters with rewards from the publisher (personal letters and scheduled broadcasts), free daily gifts between friends, transfers of a player's own resources to a friend (declined or expired ones come back), and rewards of other systems the player has not claimed yet (leaderboard cycle, team event prize, invite rewards, missed season tiers). Claim one letter or \"Claim all\" in one server operation, mark read, delete, the badge counters. Use this whenever the user wants a mailbox, inbox, mail, letters, compensation / gift letters, \"send a gift to a friend\", \"claim all\", a mail badge, or touches client.mailbox, MailboxService, MailboxDefinitions, MailboxLetter, MailboxPendingReward — even if they don't name the module. The ready screen is the `mailbox` module (modules/mailbox).",
|
|
4
|
-
"content": "---\nname: mailbox-system\ndescription: >-\n Build a mailbox / inbox in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.mailbox (MailboxService): letters with rewards\n from the publisher (personal letters and scheduled broadcasts), free daily\n gifts between friends, transfers of a player's own resources to a friend\n (declined or expired ones come back), and rewards of other systems the\n player has not claimed yet (leaderboard cycle, team event prize, invite\n rewards, missed season tiers). Claim one letter or \"Claim all\" in one\n server operation, mark read, delete, the badge counters.\n Use this whenever the user wants a mailbox, inbox, mail, letters,\n compensation / gift letters, \"send a gift to a friend\", \"claim all\", a\n mail badge, or touches client.mailbox, MailboxService, MailboxDefinitions,\n MailboxLetter, MailboxPendingReward — even if they don't name the module.\n The ready screen is the `mailbox` module (modules/mailbox).\n---\n\n# Mailbox system (iDosGames TS SDK)\n\nThe mailbox holds **value the player claims**. It is not a notification feed\n(that is Push) and not the social activity inbox (`client.social.getInbox`).\nEverything is **server-authoritative**: expiry, limits, friendship, what may be\ntransferred and every grant are the engine's. The client reads pages, calls an\naction, and renders the result; the SDK applies granted/charged resources to\nthe local cache itself.\n\nA ready screen exists: the **`mailbox` module** (tabs by category, letter popup,\nClaim / Claim all, \"Send gifts\", badge). Compose it instead of writing a new one\nunless the game needs its own look. Open it on a friend from anywhere:\n\n```ts\nctx.features.open(\"mailbox\", { friendUserID, mode: \"gift\" }); // or mode: \"transfer\"\n```\n\n## Key entities\n\n1. **`MailboxDefinitions`** — the title's `Mailbox` config section:\n `Enabled` (off by default — no mailbox, no tab), capacity and letter\n lifetimes, `Templates` (publisher texts per language), `Gifts.Social`\n (free gifts: `Grant`), `Gifts.Transfer` (`Enabled`, `AllowedResources` with\n `MaxAmountPerGift`), `Integrations` (which systems may send missed rewards).\n The client copy has NO daily limits, friendship threshold or sender gates —\n those are anti-fraud limits kept on the server; ask `getGiftStatus()`.\n2. **`MailboxLetter`** — one stored letter: `MessageID`, `Source`\n (Admin, Broadcast, SocialGift, Transfer, TransferReturn, Module, CloudCode),\n `Category` (System, Reward, Gift, Social), `Subject`/`Body` already in the\n requested language, `TemplateID` + `TemplateParams`, `Icon`, `Banner`,\n `Link` (`FeatureID` → `ctx.features.open`, or `Url`), `SenderName`,\n `SenderNote`, `Rewards` (a `ResourceGrant`), `HasRewards`, `IsTransfer`,\n `SentAt`, `ExpiresAt`, `ReadAt`, `ClaimedAt`.\n3. **`MailboxPendingReward`** — a LIVE reward of another system\n (`MessageID = \"live:{Module}:{Key}\"`). The mailbox does not store it: the\n module itself grants it, so claiming it here or on the module's screen is\n the same claim — never both.\n\n⚠ **`Subject`/`Body` can be null.** Letters of the platform's own templates\n(`system.gift`, `system.transfer`, `system.transfer_return`,\n`system.leaderboard_reward`, `system.coop_grand_prize`,\n`system.coop_object_reward`, `system.season_unclaimed`,\n`system.referral_reward`) come without text unless the publisher rewrote the\ntemplate. Render them from your own strings by `TemplateID`, filling\n`{param}` placeholders from `TemplateParams` (`sender`, `rank`, `leaderboard`,\n`event`, `season`, `tier`, `friends`).\n\n## Methods\n\nAll return `Promise<OperationResult<T>>` — branch on `ok`. Codes come back in\n`error` as plain strings (`MailboxErrors`: `ALREADY_CLAIMED`, `EXPIRED`,\n`NOT_FRIENDS`, `ALREADY_SENT`, `SEND_LIMIT`, `RECIPIENT_LIMIT`, `NOT_ENOUGH_RESOURCES`,\n`AMOUNT_TOO_SMALL`, `FRIENDSHIP_TOO_NEW`,\n`AMOUNT_TOO_LARGE`, `RESOURCE_NOT_GIFTABLE`, …) — map them to words.\n\n| Method | What it does |\n| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `getMessages({ cursor?, pageSize?, category?, language? })` | One page, newest first. The FIRST page (no cursor) also delivers broadcasts meant for this player, returns their own expired transfers, and carries `PendingRewards`, `UnreadCount`, `UnclaimedCount`. Language defaults to the client locale. |\n| `getUnreadCount()` | Badge counters only; writes nothing — safe for a menu badge. |\n| `readMessages(ids \\| \"all\")` | Mark read. |\n| `claimReward(messageID)` | One letter or one `live:` reward. |\n| `claimRewards(ids)` | Several in one atomic operation; failures come back in `Failed`. |\n| `claimAll()` | Everything claimable, letters and live rewards. |\n| `deleteMessages(ids \\| \"allRead\", { force? })` | `\"allRead\"` keeps letters with unclaimed rewards. Deleting a pending TRANSFER declines it — the resources go back to the friend. |\n| `sendGift(giftID, recipientUserIDs)` | Gift, once per period per friend (a day unless the title set `IntervalSeconds` from `AnchorUtc`). A gift with `Cost` charges the sender per friend (applied locally from `Resources`). `Sent` / `Failed` per friend. |\n| `sendTransfer(friendID, entries, { requestID, note? })` | Own resources, charged NOW, wait in the friend's letter. |\n| `getGiftStatus()` | Remaining limits of the current period (`RemainingInPeriod`, `ResetsAt`) and who already got each gift in it (`SentTo`). |\n\nClaims return ONE merged `Resources` for the whole request and the service\napplies it once — never add up the per-letter `Claimed[].Rewards` yourself.\n\n⚠ **`requestID` of a transfer is its idempotency key.** Generate one per\nattempt and keep it for retries of the SAME transfer (a timeout then replays\ninstead of charging twice — `IdempotentReplay: true`). Make a new one only after\na success.\n\n## Badge\n\nEvery mailbox response carries fresh counters and the service emits\n`mailbox:countersChanged` (`{ UnreadCount, UnclaimedCount }`). Read\n`getUnreadCount()` once on start (and when the player returns to the tab), then\njust listen — no polling. Counters are capped at `MAILBOX_COUNTER_CAP` (100):\nshow \"99+\". Unread and unclaimed overlap (a new letter with a gift is both) —\nshow the bigger one, not the sum.\n\n```ts\nconst off = client.on(\"mailbox:countersChanged\", (c) =>\n setBadge(Math.max(c.UnreadCount, c.UnclaimedCount)),\n);\nvoid client.mailbox.getUnreadCount();\n```\n\nOther events: `mailbox:messagesLoaded`, `mailbox:rewardsClaimed`,\n`mailbox:giftSent`, `mailbox:transferSent`.\n\n## Recipes\n\n### Inbox with \"Claim all\"\n\n```ts\nconst page = await client.mailbox.getMessages();\nif (!page.ok) return showError(page.error);\nrender(page.data.Messages, page.data.PendingRewards ?? []);\n\nconst all = await client.mailbox.claimAll();\nif (all.ok) celebrate(all.data.Resources); // balances already updated\n```\n\nPaging: pass `NextCursor` back as `cursor`; stop when it is null. Letters of a\nlater page never repeat earlier ones, but de-duplicate by `MessageID` anyway if\nthe list was refreshed in between.\n\n### Gift every friend who has not got one today\n\n```ts\nconst status = await client.mailbox.getGiftStatus();\nconst sent = new Set(\n status.ok ? (status.data.Social[giftID]?.SentTo ?? []) : [],\n);\nconst friends = await client.social.getFriendsList();\nconst open = (friends.ok ? (friends.data.Friends ?? []) : [])\n .map((f) => f.UserID)\n .filter((id) => !sent.has(id));\nawait client.mailbox.sendGift(giftID, open);\n```\n\n### Transfer coins to a friend\n\n```ts\nconst requestID = crypto.randomUUID(); // keep it for retries of THIS transfer\nconst r = await client.mailbox.sendTransfer(\n friendID,\n [{ Type: \"VirtualCurrency\", CurrencyID: \"CO\", Amount: 100 }],\n { requestID, note: \"good luck\" },\n);\n```\n\nOnly resources of `Gifts.Transfer.AllowedResources` can go — `MaxAmountPerGift` is a CAP (any\namount from 1 up to it), entries go separately or together up to `MaxEntriesPerGift`. `FeePercent`\nis withheld from what the friend gets (show `RecipientReceives`), `FeeCost` is charged on top; both\nburn only when the friend claims — a declined or expired transfer returns everything charged; unstackable item instances are never transferable. Friends\nmust be friends long enough (`FRIENDSHIP_TOO_NEW`) — the threshold is not in\nthe client config.\n\n## Gotchas\n\n- **Mailbox off = no calls.** With `Enabled !== true` every action fails with\n `MAILBOX_DISABLED`; hide the tab instead.\n- **Crypto currency never travels by mail** — neither in letters nor\n transfers (`CRYPTO_FORBIDDEN`).\n- **A letter expires** (`ExpiresAt`): its reward can no longer be claimed; a\n transfer that expires goes back to the sender automatically.\n- **Sending letters is not a client action.** Letters come from the publisher\n dashboard (personal letters, broadcasts by segment and schedule), from\n CloudCode (`server.SendMail`), or from the engine's own systems. A client\n can only gift and transfer between friends.\n",
|
|
4
|
+
"content": "---\nname: mailbox-system\ndescription: >-\n Build a mailbox / inbox in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.mailbox (MailboxService): letters with rewards\n from the publisher (personal letters and scheduled broadcasts), free daily\n gifts between friends, transfers of a player's own resources to a friend\n (declined or expired ones come back), and rewards of other systems the\n player has not claimed yet (leaderboard cycle, team event prize, invite\n rewards, missed season tiers). Claim one letter or \"Claim all\" in one\n server operation, mark read, delete, the badge counters.\n Use this whenever the user wants a mailbox, inbox, mail, letters,\n compensation / gift letters, \"send a gift to a friend\", \"claim all\", a\n mail badge, or touches client.mailbox, MailboxService, MailboxDefinitions,\n MailboxLetter, MailboxPendingReward — even if they don't name the module.\n The ready screen is the `mailbox` module (modules/mailbox).\n---\n\n# Mailbox system (iDosGames TS SDK)\n\nThe mailbox holds **value the player claims**. It is not a notification feed\n(that is Push) and not the social activity inbox (`client.social.getInbox`).\nEverything is **server-authoritative**: expiry, limits, friendship, what may be\ntransferred and every grant are the engine's. The client reads pages, calls an\naction, and renders the result; the SDK applies granted/charged resources to\nthe local cache itself.\n\nA ready screen exists: the **`mailbox` module** (tabs by category, letter popup,\nClaim / Claim all, \"Send gifts\", badge). Compose it instead of writing a new one\nunless the game needs its own look. Open it on a friend from anywhere:\n\n```ts\nctx.features.open(\"mailbox\", { friendUserID, mode: \"gift\" }); // or mode: \"transfer\"\n```\n\n## Key entities\n\n1. **`MailboxDefinitions`** — the title's `Mailbox` config section:\n `Enabled` (off by default — no mailbox, no tab), capacity and letter\n lifetimes, `Templates` (publisher texts per language), `Gifts.Social`\n (free gifts: `Grant`), `Gifts.Transfer` (`Enabled`, `AllowedResources` with\n `MaxAmountPerGift`), `Integrations` (which systems may send missed rewards).\n The client copy has NO daily limits, friendship threshold or sender gates —\n those are anti-fraud limits kept on the server; ask `getGiftStatus()`.\n2. **`MailboxLetter`** — one stored letter: `MessageID`, `Source`\n (Admin, Broadcast, SocialGift, Transfer, TransferReturn, Module, CloudCode),\n `Category` (System, Reward, Gift, Social), `Subject`/`Body` already in the\n requested language, `TemplateID` + `TemplateParams`, `Icon`, `Banner`,\n `Link` (`FeatureID` → `ctx.features.open`, or `Url`), `SenderName`,\n `SenderNote`, `Rewards` (a `ResourceGrant`), `HasRewards`, `IsTransfer`,\n `SentAt`, `ExpiresAt`, `ReadAt`, `ClaimedAt`.\n3. **`MailboxPendingReward`** — a LIVE reward of another system\n (`MessageID = \"live:{Module}:{Key}\"`). The mailbox does not store it: the\n module itself grants it, so claiming it here or on the module's screen is\n the same claim — never both.\n\n⚠ **`Subject`/`Body` can be null.** Letters of the platform's own templates\n(`system.gift`, `system.transfer`, `system.transfer_return`,\n`system.leaderboard_reward`, `system.coop_grand_prize`,\n`system.coop_object_reward`, `system.season_unclaimed`,\n`system.referral_reward`) come without text unless the publisher rewrote the\ntemplate. Render them from your own strings by `TemplateID`, filling\n`{param}` placeholders from `TemplateParams` (`sender`, `rank`, `leaderboard`,\n`event`, `season`, `tier`, `friends`).\n\n## Methods\n\nAll return `Promise<OperationResult<T>>` — branch on `ok`. Codes come back in\n`error` as plain strings (`MailboxErrors`: `ALREADY_CLAIMED`, `EXPIRED`,\n`NOT_FRIENDS`, `ALREADY_SENT`, `SEND_LIMIT`, `RECIPIENT_LIMIT`, `NOT_ENOUGH_RESOURCES`,\n`AMOUNT_TOO_SMALL`, `FRIENDSHIP_TOO_NEW`,\n`AMOUNT_TOO_LARGE`, `RESOURCE_NOT_GIFTABLE`, …) — map them to words.\n\n| Method | What it does |\n| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `getMessages({ cursor?, pageSize?, category?, language? })` | One page, newest first. The FIRST page (no cursor) also delivers broadcasts meant for this player, returns their own expired transfers, and carries `PendingRewards`, `UnreadCount`, `UnclaimedCount`. Language defaults to the client locale. |\n| `getUnreadCount()` | Badge counters only; writes nothing — safe for a menu badge. |\n| `readMessages(ids \\| \"all\")` | Mark read. |\n| `claimReward(messageID)` | One letter or one `live:` reward. |\n| `claimRewards(ids)` | Several in one atomic operation; failures come back in `Failed`. |\n| `claimAll()` | Everything claimable, letters and live rewards. |\n| `deleteMessages(ids \\| \"allRead\", { force? })` | `\"allRead\"` keeps letters with unclaimed rewards. Deleting a pending TRANSFER declines it — the resources go back to the friend. |\n| `sendGift(giftID, recipientUserIDs)` | Gift, once per period per friend (a day unless the title set `IntervalSeconds` from `AnchorUtc`). A gift with `Cost` charges the sender per friend (applied locally from `Resources`). `Sent` / `Failed` per friend. |\n| `sendTransfer(friendID, entries, { requestID, note? })` | Own resources, charged NOW, wait in the friend's letter. |\n| `getGiftStatus()` | Remaining limits of the current period (`RemainingInPeriod`, `ResetsAt`) and who already got each gift in it (`SentTo`). |\n\nClaims return ONE merged `Resources` for the whole request and the service\napplies it once — never add up the per-letter `Claimed[].Rewards` yourself.\n\n⚠ **`requestID` of a transfer is its idempotency key.** Generate one per\nattempt and keep it for retries of the SAME transfer (a timeout then replays\ninstead of charging twice — `IdempotentReplay: true`). Make a new one only after\na success.\n\n## Badge\n\nEvery mailbox response carries fresh counters and the service emits\n`mailbox:countersChanged` (`{ UnreadCount, UnclaimedCount }`) and puts them into the\ncentral cache. Read `getUnreadCount()` on start (and when the player returns to the\ntab) and set the badge FROM ITS ANSWER — the counters usually come with the login\n(core ≥ 0.21, `ClientState.Views`) before your listener exists, and a read the cache\nanswers emits nothing; then listen for the actions. Counters are capped at `MAILBOX_COUNTER_CAP` (100):\nshow \"99+\". Unread and unclaimed overlap (a new letter with a gift is both) —\nshow the bigger one, not the sum.\n\n```ts\nconst show = (c: { UnreadCount: number; UnclaimedCount: number }) =>\n setBadge(Math.max(c.UnreadCount, c.UnclaimedCount));\nconst off = client.on(\"mailbox:countersChanged\", show);\nvoid client.mailbox.getUnreadCount().then((r) => r.ok && show(r.data));\n```\n\nOther events: `mailbox:messagesLoaded`, `mailbox:rewardsClaimed`,\n`mailbox:giftSent`, `mailbox:transferSent`.\n\n## Recipes\n\n### Inbox with \"Claim all\"\n\n```ts\nconst page = await client.mailbox.getMessages();\nif (!page.ok) return showError(page.error);\nrender(page.data.Messages, page.data.PendingRewards ?? []);\n\nconst all = await client.mailbox.claimAll();\nif (all.ok) celebrate(all.data.Resources); // balances already updated\n```\n\nPaging: pass `NextCursor` back as `cursor`; stop when it is null. Letters of a\nlater page never repeat earlier ones, but de-duplicate by `MessageID` anyway if\nthe list was refreshed in between.\n\n### Gift every friend who has not got one today\n\n```ts\nconst status = await client.mailbox.getGiftStatus();\nconst sent = new Set(\n status.ok ? (status.data.Social[giftID]?.SentTo ?? []) : [],\n);\nconst friends = await client.social.getFriendsList();\nconst open = (friends.ok ? (friends.data.Friends ?? []) : [])\n .map((f) => f.UserID)\n .filter((id) => !sent.has(id));\nawait client.mailbox.sendGift(giftID, open);\n```\n\n### Transfer coins to a friend\n\n```ts\nconst requestID = crypto.randomUUID(); // keep it for retries of THIS transfer\nconst r = await client.mailbox.sendTransfer(\n friendID,\n [{ Type: \"VirtualCurrency\", CurrencyID: \"CO\", Amount: 100 }],\n { requestID, note: \"good luck\" },\n);\n```\n\nOnly resources of `Gifts.Transfer.AllowedResources` can go — `MaxAmountPerGift` is a CAP (any\namount from 1 up to it), entries go separately or together up to `MaxEntriesPerGift`. `FeePercent`\nis withheld from what the friend gets (show `RecipientReceives`), `FeeCost` is charged on top; both\nburn only when the friend claims — a declined or expired transfer returns everything charged; unstackable item instances are never transferable. Friends\nmust be friends long enough (`FRIENDSHIP_TOO_NEW`) — the threshold is not in\nthe client config.\n\n## Gotchas\n\n- **Mailbox off = no calls.** With `Enabled !== true` every action fails with\n `MAILBOX_DISABLED`; hide the tab instead.\n- **Crypto currency never travels by mail** — neither in letters nor\n transfers (`CRYPTO_FORBIDDEN`).\n- **A letter expires** (`ExpiresAt`): its reward can no longer be claimed; a\n transfer that expires goes back to the sender automatically.\n- **Sending letters is not a client action.** Letters come from the publisher\n dashboard (personal letters, broadcasts by segment and schedule), from\n CloudCode (`server.SendMail`), or from the engine's own systems. A client\n can only gift and transfer between friends.\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|