@idosgames/mcp 0.1.26 → 0.1.28
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 +1 -1
- package/registry/index.json +32 -32
- package/registry/modules/ai.json +1 -1
- package/registry/modules/board-game.json +1 -1
- package/registry/modules/boosts.json +1 -1
- package/registry/modules/character.json +1 -1
- package/registry/modules/chat.json +1 -1
- package/registry/modules/collections.json +1 -1
- package/registry/modules/coop-events.json +1 -1
- package/registry/modules/craft.json +1 -1
- package/registry/modules/deals.json +1 -1
- package/registry/modules/events.json +1 -1
- package/registry/modules/friends.json +1 -1
- package/registry/modules/idle-rpg.json +1 -1
- package/registry/modules/inventory.json +1 -1
- package/registry/modules/invite.json +1 -1
- package/registry/modules/leaderboard.json +1 -1
- package/registry/modules/lootboxes.json +1 -1
- package/registry/modules/mailbox.json +1 -1
- package/registry/modules/marketplace.json +1 -1
- package/registry/modules/mini-game.json +1 -1
- package/registry/modules/multiplayer.json +1 -1
- package/registry/modules/premium.json +1 -1
- package/registry/modules/profile.json +1 -1
- package/registry/modules/pvp.json +1 -1
- package/registry/modules/quests.json +1 -1
- package/registry/modules/rewards.json +1 -1
- package/registry/modules/season.json +1 -1
- package/registry/modules/store.json +1 -1
- package/registry/modules/tutorials.json +1 -1
- package/registry/modules/voxelcraft.json +1 -1
- package/registry/modules/workshop.json +1 -1
- package/registry/skills/blockchain-system.json +1 -1
- package/registry/skills/currency-system.json +1 -1
- package/registry/skills/idosgames-getting-started.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idosgames/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "MCP server that serves the iDosGames Module & Skills Registry to AI coding agents (Claude Code, Codex, Cursor…): list/pull composable game modules and the host scaffold, and load skills for @idosgames/core, the module contract, and composition.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/registry/host.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "package.json",
|
|
26
|
-
"content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (src/base/) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.12.0\",\n \"@idosgames/core\": \"0.21.
|
|
26
|
+
"content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (src/base/) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.12.0\",\n \"@idosgames/core\": \"0.21.2\",\n \"@idosgames/module-sdk\": \"0.10.0\",\n \"@idosgames/react\": \"0.4.4\",\n \"@idosgames/wallet\": \"0.6.2\",\n \"@reown/appkit\": \"1.8.23\",\n \"@reown/appkit-adapter-wagmi\": \"1.8.23\",\n \"@tanstack/react-query\": \"5.101.2\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\",\n \"wagmi\": \"3.7.2\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"6.0.4\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"8.1.5\"\n }\n}\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "public/sw.js",
|
package/registry/index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedFromCommit": "
|
|
2
|
+
"generatedFromCommit": "785a4577dbda0aa6d355f2b43e62e9f6a3537471",
|
|
3
3
|
"runtimePackages": {
|
|
4
|
-
"@idosgames/core": "0.21.
|
|
4
|
+
"@idosgames/core": "0.21.2",
|
|
5
5
|
"@idosgames/wallet": "0.6.2",
|
|
6
6
|
"@idosgames/module-sdk": "0.10.0",
|
|
7
7
|
"@idosgames/react": "0.4.4",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"version": "0.1.1",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@idosgames/core": "0.21.
|
|
48
|
+
"@idosgames/core": "0.21.2",
|
|
49
49
|
"@idosgames/module-sdk": "0.10.0",
|
|
50
50
|
"@idosgames/react": "0.4.4",
|
|
51
51
|
"react": "19.2.7"
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"version": "0.1.1",
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@idosgames/core": "0.21.
|
|
85
|
+
"@idosgames/core": "0.21.2",
|
|
86
86
|
"@idosgames/module-sdk": "0.10.0",
|
|
87
87
|
"@idosgames/react": "0.4.4",
|
|
88
88
|
"react": "19.2.7"
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
142
|
"dependencies": {
|
|
143
|
-
"@idosgames/core": "0.21.
|
|
143
|
+
"@idosgames/core": "0.21.2",
|
|
144
144
|
"@idosgames/module-sdk": "0.10.0",
|
|
145
145
|
"@idosgames/react": "0.4.4",
|
|
146
146
|
"react": "19.2.7"
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
},
|
|
178
178
|
"version": "0.1.0",
|
|
179
179
|
"dependencies": {
|
|
180
|
-
"@idosgames/core": "0.21.
|
|
180
|
+
"@idosgames/core": "0.21.2",
|
|
181
181
|
"@idosgames/module-sdk": "0.10.0",
|
|
182
182
|
"@idosgames/react": "0.4.4",
|
|
183
183
|
"react": "19.2.7"
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
},
|
|
215
215
|
"version": "0.1.0",
|
|
216
216
|
"dependencies": {
|
|
217
|
-
"@idosgames/core": "0.21.
|
|
217
|
+
"@idosgames/core": "0.21.2",
|
|
218
218
|
"@idosgames/module-sdk": "0.10.0",
|
|
219
219
|
"@idosgames/react": "0.4.4",
|
|
220
220
|
"react": "19.2.7"
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
},
|
|
251
251
|
"version": "0.1.0",
|
|
252
252
|
"dependencies": {
|
|
253
|
-
"@idosgames/core": "0.21.
|
|
253
|
+
"@idosgames/core": "0.21.2",
|
|
254
254
|
"@idosgames/module-sdk": "0.10.0",
|
|
255
255
|
"@idosgames/react": "0.4.4",
|
|
256
256
|
"react": "19.2.7"
|
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
},
|
|
292
292
|
"version": "0.1.1",
|
|
293
293
|
"dependencies": {
|
|
294
|
-
"@idosgames/core": "0.21.
|
|
294
|
+
"@idosgames/core": "0.21.2",
|
|
295
295
|
"@idosgames/module-sdk": "0.10.0",
|
|
296
296
|
"@idosgames/react": "0.4.4",
|
|
297
297
|
"react": "19.2.7"
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
},
|
|
328
328
|
"version": "0.1.1",
|
|
329
329
|
"dependencies": {
|
|
330
|
-
"@idosgames/core": "0.21.
|
|
330
|
+
"@idosgames/core": "0.21.2",
|
|
331
331
|
"@idosgames/module-sdk": "0.10.0",
|
|
332
332
|
"@idosgames/react": "0.4.4",
|
|
333
333
|
"react": "19.2.7"
|
|
@@ -366,7 +366,7 @@
|
|
|
366
366
|
},
|
|
367
367
|
"version": "0.1.0",
|
|
368
368
|
"dependencies": {
|
|
369
|
-
"@idosgames/core": "0.21.
|
|
369
|
+
"@idosgames/core": "0.21.2",
|
|
370
370
|
"@idosgames/module-sdk": "0.10.0",
|
|
371
371
|
"@idosgames/react": "0.4.4",
|
|
372
372
|
"react": "19.2.7"
|
|
@@ -405,7 +405,7 @@
|
|
|
405
405
|
},
|
|
406
406
|
"version": "0.1.1",
|
|
407
407
|
"dependencies": {
|
|
408
|
-
"@idosgames/core": "0.21.
|
|
408
|
+
"@idosgames/core": "0.21.2",
|
|
409
409
|
"@idosgames/module-sdk": "0.10.0",
|
|
410
410
|
"@idosgames/react": "0.4.4",
|
|
411
411
|
"react": "19.2.7"
|
|
@@ -445,7 +445,7 @@
|
|
|
445
445
|
},
|
|
446
446
|
"version": "0.1.1",
|
|
447
447
|
"dependencies": {
|
|
448
|
-
"@idosgames/core": "0.21.
|
|
448
|
+
"@idosgames/core": "0.21.2",
|
|
449
449
|
"@idosgames/module-sdk": "0.10.0",
|
|
450
450
|
"@idosgames/react": "0.4.4",
|
|
451
451
|
"react": "19.2.7"
|
|
@@ -486,7 +486,7 @@
|
|
|
486
486
|
},
|
|
487
487
|
"version": "0.1.1",
|
|
488
488
|
"dependencies": {
|
|
489
|
-
"@idosgames/core": "0.21.
|
|
489
|
+
"@idosgames/core": "0.21.2",
|
|
490
490
|
"@idosgames/module-sdk": "0.10.0",
|
|
491
491
|
"@idosgames/react": "0.4.4",
|
|
492
492
|
"react": "19.2.7"
|
|
@@ -525,7 +525,7 @@
|
|
|
525
525
|
},
|
|
526
526
|
"version": "0.1.1",
|
|
527
527
|
"dependencies": {
|
|
528
|
-
"@idosgames/core": "0.21.
|
|
528
|
+
"@idosgames/core": "0.21.2",
|
|
529
529
|
"@idosgames/module-sdk": "0.10.0",
|
|
530
530
|
"@idosgames/react": "0.4.4",
|
|
531
531
|
"react": "19.2.7"
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
},
|
|
568
568
|
"version": "0.1.1",
|
|
569
569
|
"dependencies": {
|
|
570
|
-
"@idosgames/core": "0.21.
|
|
570
|
+
"@idosgames/core": "0.21.2",
|
|
571
571
|
"@idosgames/module-sdk": "0.10.0",
|
|
572
572
|
"@idosgames/react": "0.4.4",
|
|
573
573
|
"react": "19.2.7"
|
|
@@ -604,7 +604,7 @@
|
|
|
604
604
|
},
|
|
605
605
|
"version": "0.1.1",
|
|
606
606
|
"dependencies": {
|
|
607
|
-
"@idosgames/core": "0.21.
|
|
607
|
+
"@idosgames/core": "0.21.2",
|
|
608
608
|
"@idosgames/module-sdk": "0.10.0",
|
|
609
609
|
"@idosgames/react": "0.4.4",
|
|
610
610
|
"react": "19.2.7"
|
|
@@ -645,7 +645,7 @@
|
|
|
645
645
|
},
|
|
646
646
|
"version": "0.1.1",
|
|
647
647
|
"dependencies": {
|
|
648
|
-
"@idosgames/core": "0.21.
|
|
648
|
+
"@idosgames/core": "0.21.2",
|
|
649
649
|
"@idosgames/module-sdk": "0.10.0",
|
|
650
650
|
"@idosgames/react": "0.4.4",
|
|
651
651
|
"react": "19.2.7"
|
|
@@ -685,7 +685,7 @@
|
|
|
685
685
|
},
|
|
686
686
|
"version": "0.1.1",
|
|
687
687
|
"dependencies": {
|
|
688
|
-
"@idosgames/core": "0.21.
|
|
688
|
+
"@idosgames/core": "0.21.2",
|
|
689
689
|
"@idosgames/module-sdk": "0.10.0",
|
|
690
690
|
"@idosgames/react": "0.4.4",
|
|
691
691
|
"react": "19.2.7"
|
|
@@ -725,7 +725,7 @@
|
|
|
725
725
|
},
|
|
726
726
|
"version": "0.1.1",
|
|
727
727
|
"dependencies": {
|
|
728
|
-
"@idosgames/core": "0.21.
|
|
728
|
+
"@idosgames/core": "0.21.2",
|
|
729
729
|
"@idosgames/module-sdk": "0.10.0",
|
|
730
730
|
"@idosgames/react": "0.4.4",
|
|
731
731
|
"react": "19.2.7"
|
|
@@ -765,7 +765,7 @@
|
|
|
765
765
|
},
|
|
766
766
|
"version": "0.1.1",
|
|
767
767
|
"dependencies": {
|
|
768
|
-
"@idosgames/core": "0.21.
|
|
768
|
+
"@idosgames/core": "0.21.2",
|
|
769
769
|
"@idosgames/module-sdk": "0.10.0",
|
|
770
770
|
"@idosgames/react": "0.4.4",
|
|
771
771
|
"react": "19.2.7"
|
|
@@ -806,7 +806,7 @@
|
|
|
806
806
|
},
|
|
807
807
|
"version": "0.1.0",
|
|
808
808
|
"dependencies": {
|
|
809
|
-
"@idosgames/core": "0.21.
|
|
809
|
+
"@idosgames/core": "0.21.2",
|
|
810
810
|
"@idosgames/module-sdk": "0.10.0",
|
|
811
811
|
"@idosgames/react": "0.4.4",
|
|
812
812
|
"react": "19.2.7"
|
|
@@ -844,7 +844,7 @@
|
|
|
844
844
|
},
|
|
845
845
|
"version": "0.1.0",
|
|
846
846
|
"dependencies": {
|
|
847
|
-
"@idosgames/core": "0.21.
|
|
847
|
+
"@idosgames/core": "0.21.2",
|
|
848
848
|
"@idosgames/module-sdk": "0.10.0",
|
|
849
849
|
"@idosgames/react": "0.4.4",
|
|
850
850
|
"react": "19.2.7"
|
|
@@ -884,7 +884,7 @@
|
|
|
884
884
|
},
|
|
885
885
|
"version": "0.1.1",
|
|
886
886
|
"dependencies": {
|
|
887
|
-
"@idosgames/core": "0.21.
|
|
887
|
+
"@idosgames/core": "0.21.2",
|
|
888
888
|
"@idosgames/module-sdk": "0.10.0",
|
|
889
889
|
"@idosgames/react": "0.4.4",
|
|
890
890
|
"react": "19.2.7"
|
|
@@ -923,7 +923,7 @@
|
|
|
923
923
|
},
|
|
924
924
|
"version": "0.1.3",
|
|
925
925
|
"dependencies": {
|
|
926
|
-
"@idosgames/core": "0.21.
|
|
926
|
+
"@idosgames/core": "0.21.2",
|
|
927
927
|
"@idosgames/module-sdk": "0.10.0",
|
|
928
928
|
"@idosgames/react": "0.4.4",
|
|
929
929
|
"react": "19.2.7"
|
|
@@ -960,7 +960,7 @@
|
|
|
960
960
|
},
|
|
961
961
|
"version": "0.1.1",
|
|
962
962
|
"dependencies": {
|
|
963
|
-
"@idosgames/core": "0.21.
|
|
963
|
+
"@idosgames/core": "0.21.2",
|
|
964
964
|
"@idosgames/module-sdk": "0.10.0",
|
|
965
965
|
"@idosgames/react": "0.4.4",
|
|
966
966
|
"react": "19.2.7"
|
|
@@ -1004,7 +1004,7 @@
|
|
|
1004
1004
|
},
|
|
1005
1005
|
"version": "0.1.2",
|
|
1006
1006
|
"dependencies": {
|
|
1007
|
-
"@idosgames/core": "0.21.
|
|
1007
|
+
"@idosgames/core": "0.21.2",
|
|
1008
1008
|
"@idosgames/module-sdk": "0.10.0",
|
|
1009
1009
|
"@idosgames/react": "0.4.4",
|
|
1010
1010
|
"react": "19.2.7"
|
|
@@ -1039,7 +1039,7 @@
|
|
|
1039
1039
|
},
|
|
1040
1040
|
"version": "0.1.1",
|
|
1041
1041
|
"dependencies": {
|
|
1042
|
-
"@idosgames/core": "0.21.
|
|
1042
|
+
"@idosgames/core": "0.21.2",
|
|
1043
1043
|
"@idosgames/module-sdk": "0.10.0",
|
|
1044
1044
|
"@idosgames/react": "0.4.4",
|
|
1045
1045
|
"react": "19.2.7"
|
|
@@ -1089,7 +1089,7 @@
|
|
|
1089
1089
|
]
|
|
1090
1090
|
},
|
|
1091
1091
|
"dependencies": {
|
|
1092
|
-
"@idosgames/core": "0.21.
|
|
1092
|
+
"@idosgames/core": "0.21.2",
|
|
1093
1093
|
"@idosgames/module-sdk": "0.10.0",
|
|
1094
1094
|
"@idosgames/react": "0.4.4",
|
|
1095
1095
|
"@idosgames/wallet": "0.6.2",
|
|
@@ -1171,7 +1171,7 @@
|
|
|
1171
1171
|
]
|
|
1172
1172
|
},
|
|
1173
1173
|
"dependencies": {
|
|
1174
|
-
"@idosgames/core": "0.21.
|
|
1174
|
+
"@idosgames/core": "0.21.2",
|
|
1175
1175
|
"@idosgames/module-sdk": "0.10.0",
|
|
1176
1176
|
"@idosgames/react": "0.4.4",
|
|
1177
1177
|
"phaser": "4.2.1",
|
|
@@ -1246,7 +1246,7 @@
|
|
|
1246
1246
|
},
|
|
1247
1247
|
"version": "0.4.1",
|
|
1248
1248
|
"dependencies": {
|
|
1249
|
-
"@idosgames/core": "0.21.
|
|
1249
|
+
"@idosgames/core": "0.21.2",
|
|
1250
1250
|
"@idosgames/module-sdk": "0.10.0",
|
|
1251
1251
|
"three": "0.185.1"
|
|
1252
1252
|
},
|
|
@@ -1280,7 +1280,7 @@
|
|
|
1280
1280
|
},
|
|
1281
1281
|
"version": "0.1.1",
|
|
1282
1282
|
"dependencies": {
|
|
1283
|
-
"@idosgames/core": "0.21.
|
|
1283
|
+
"@idosgames/core": "0.21.2",
|
|
1284
1284
|
"@idosgames/module-sdk": "0.10.0",
|
|
1285
1285
|
"@idosgames/react": "0.4.4",
|
|
1286
1286
|
"react": "19.2.7"
|
package/registry/modules/ai.json
CHANGED
|
@@ -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 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 EVERY deposit in full (there is no deposit\n split since 04.10.2026), plus any `donateToUsersPool` donations, and\n reduced by player withdrawals and by crypto players spend in-game (spent\n crypto moves to the developer share). On a player withdrawal the developer\n fee (`CryptoCurrencyDefinition.DeveloperWithdrawalFeePercent`), the\n Community Marketing fee (`CommunityMarketingWithdrawalFeePercent`) and the\n burn are withheld (together at most 50%), plus the platform fee on top. 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
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"references": [
|
|
6
6
|
{
|
|
7
7
|
"path": "data-model.md",
|
|
8
|
-
"content": "# Currency data model — reference\n\nFull shape of the `CurrencyDefinitions` config, and — the canonical\ndocumentation for the whole SDK — the shared `ResourceConsume` /\n`ResourceGrant` / `ResourceOperation` / `ResourceEntry` cost-and-reward\nprimitives. All types are **strictly typed** and exported from\n`@idosgames/core`; every object schema keeps `.passthrough()`, so a field the\nbackend adds later still round-trips instead of being stripped. Field names\nare PascalCase (straight from the backend JSON).\n\n## Contents\n\n- [Config: CurrencyDefinitions](#config-currencydefinitions)\n- [VirtualCurrencyDefinition](#virtualcurrencydefinition)\n- [CryptoCurrencyDefinition](#cryptocurrencydefinition)\n- [Shared conversion config](#shared-conversion-config)\n- [Backend conversion formulas](#backend-conversion-formulas) — transcribed from `ConversionService.cs`\n- [The shared resource primitives](#the-shared-resource-primitives) — canonical home\n - [ResourceEntry](#resourceentry)\n - [ResourceBundle](#resourcebundle)\n - [PremiumTierBundle](#premiumtierbundle)\n - [ResourceGrant](#resourcegrant)\n - [ResourceConsume](#resourceconsume)\n - [ResourceOperation](#resourceoperation)\n - [EventTokenOperation / EventTokenAddress](#eventtokenoperation--eventtokenaddress)\n - [ResourceDualPartyResult / ResourceTransferResult](#resourcedualpartyresult--resourcetransferresult)\n- [How the SDK applies a ResourceOperation](#how-the-sdk-applies-a-resourceoperation)\n\n---\n\n## Config: CurrencyDefinitions\n\n```ts\ninterface CurrencyDefinitions {\n VirtualCurrencies?: Record<string, VirtualCurrencyDefinition> | null;\n CryptoCurrencies?: Record<string, CryptoCurrencyDefinition> | null;\n}\n```\n\nKey in both maps is the `CurrencyID`. A currency is \"known\" iff it has an\nentry in one of these maps under its `CurrencyType` (`Virtual` or `Crypto`).\n\n---\n\n## VirtualCurrencyDefinition\n\n```ts\ninterface VirtualCurrencyDefinition {\n CurrencyID: string;\n DisplayName?: string;\n AssetPaths?: Record<string, string>; // \"icon\", ...\n Economy?: {\n ValueInUSD?: string; // decimal string\n ValueInUSDUpdatedAt?: string; // ISO timestamp\n InitialDeposit?: number; // starting balance for new players\n MinBalance?: number;\n MaxBalance?: number;\n DailyEarnLimit?: number;\n DailySpendLimit?: number;\n };\n Recharge?: {\n // Energy-style auto-regen, credited in BATCHES: every FULL `Period` seconds the\n // player gets `Rate` units at once, up to `Max`. Rate=5/Period=60 means \"+5 once a\n // minute\", NOT \"+1 every 12 seconds\" — an incomplete period credits nothing.\n // The batch is clipped exactly at `Max` (if less than Rate is missing, only the\n // remainder is credited); at or above `Max` nothing is credited.\n // `Max` is the auto-recharge cap only — explicit grants may exceed it, up to\n // Economy.MaxBalance.\n Rate?: number;\n Max?: number;\n Period?: number;\n };\n Conversion?: CurrencyConversion; // see below\n Permissions?: {\n IsTradable?: boolean;\n IsPurchasable?: boolean;\n IsRefundable?: boolean;\n };\n Audit?: { CreatedAt?: string; UpdatedAt?: string };\n Status?: \"Active\" | \"Hidden\" | \"Deprecated\" | \"Maintenance\";\n}\n```\n\n`Status` governs whether the currency is usable/visible; `\"Maintenance\"` /\n`\"Deprecated\"` currencies typically reject conversions server-side even if\n`Conversion.Enabled` is true.\n\n---\n\n## CryptoCurrencyDefinition\n\n```ts\ninterface CryptoCurrencyDefinition {\n CurrencyID: string;\n DisplayName?: string;\n AssetPaths?: Record<string, string>;\n DisplayDecimals?: number; // UI rounding, not wire precision\n ValueInUSD?: string; // decimal string\n ValueInUSDUpdatedAt?: string;\n DeveloperDepositSharePercent?: string; // decimal string; see blockchain-system\n Networks?: CryptoNetworkBinding[]; // per-chain bindings\n Limits?: {\n DailyWithdrawUsd?: string;\n MonthlyWithdrawUsd?: string;\n KycRequiredAboveUsd?: string;\n };\n Permissions?: {\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n SpendableInGame?: boolean;\n ConvertibleToVirtual?: boolean; // gates cryptoConvert eligibility\n };\n Conversion?: CurrencyConversion;\n Audit?: { CreatedAt?: string; UpdatedAt?: string };\n Status?: \"Active\" | \"Hidden\" | \"Deprecated\" | \"Maintenance\";\n}\n\ninterface CryptoNetworkBinding {\n NetworkID: string;\n ContractAddress?: string;\n Decimals?: number; // on-chain token decimals\n MinWithdraw?: string;\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n}\n```\n\nDeposit/withdrawal flows (and `Networks`/`Limits` enforcement for those flows)\nbelong to the Blockchain module — see the blockchain-system skill.\n`Permissions.ConvertibleToVirtual` is the flag most relevant here: it's what\nlets a crypto balance participate in `cryptoConvert`.\n\n---\n\n## Shared conversion config\n\nBoth currency kinds reuse the same `CurrencyConversion` shape for their\n`Conversion` field:\n\n```ts\ninterface CurrencyConversion {\n Enabled?: boolean;\n RateMode?: \"Automatic\" | \"Manual\";\n Fee?: number; // SHARE of the debited amount: 0.05 = 5% (never 0..100)\n Targets?: ConversionTarget[]; // whitelist of valid conversion targets\n}\n\ninterface ConversionTarget {\n TargetCurrencyType?: \"Virtual\" | \"Crypto\";\n TargetCurrencyID: string;\n Rate?: string; // decimal string; used when RateMode is \"Manual\"\n MinAmount?: number;\n MaxAmount?: number;\n DailyLimit?: number;\n}\n```\n\nA conversion is only accepted if the source currency's `Conversion.Enabled`\nis true and the target appears in `Targets` (by type + id). `\"Automatic\"`\nrate mode means the backend derives the rate from each side's `ValueInUSD`;\n`\"Manual\"` uses the `Rate` pinned on the `ConversionTarget`. Either way, treat\n`RateApplied` on the response as the source of truth — don't recompute it.\n\n---\n\n## Backend conversion formulas\n\nTranscribed from `ConversionService.ConvertAsync` /\n`ConversionService.ConvertCryptoAsync` in the backend\n(`IDosGamesSDK/API/Client/v2/Currency/Services/ConversionService.cs`). These\nare enforced server-side; the SDK never recomputes them — use this section\nonly for building accurate cost/reward **previews**, not for validating a\nconversion before sending it.\n\n**Order of checks** (any failure short-circuits, no partial debit):\n\n1. Basic shape: non-empty `SourceID`/`TargetID`, positive amount, source ≠\n target.\n2. `Convert` (VC↔VC) rejects a `Crypto` source outright (\"Convert supports\n VC↔VC only\") and a `Crypto` target outright (\"Virtual→Crypto conversion is\n not supported\"). `CryptoConvert` rejects a non-`Crypto` source outright\n (\"CryptoConvert requires source to be Crypto\").\n3. **Status**: source or target `Maintenance` → rejected on that side (\"is\n under maintenance\"). Target (only) `Deprecated` → rejected (\"is deprecated\n and cannot receive new credits\"). A `Deprecated` **source** is allowed —\n deprecating a currency only stops new inflow, it doesn't trap the player's\n remaining balance.\n4. Source's `Conversion` must be non-null and `Enabled`, and must have a\n `Targets` entry matching `(TargetCurrencyType, TargetCurrencyID)` exactly —\n otherwise \"Conversion from 'X' to 'Y' is not allowed.\"\n5. Crypto-source → Virtual-target additionally requires\n `CryptoCurrencyPermissions.ConvertibleToVirtual` — false rejects even a\n listed target.\n6. Per-pair `MinAmount`/`MaxAmount` on the matched `ConversionTarget`, checked\n against the raw source amount before fee.\n7. **Rate resolution**:\n - `RateMode = Manual` → `rate = ConversionTarget.Rate`; a pair configured\n Manual with no `Rate` set is rejected (\"Manual conversion rate is not set\n for this pair\"), not treated as 0 or 1.\n - `RateMode = Automatic` → `rate = source.ValueInUSD / target.ValueInUSD`.\n Either side missing/zero `ValueInUSD` rejects the conversion (\"Automatic\n rate cannot be computed\").\n8. **Fee**: `Fee` is a SHARE (`0.05` = 5%), so `feeAmount = sourceAmount * Fee`\n — there is no division by 100. It is **not** clamped to a range: only `NaN`\n is treated as zero, and the fee is capped at the source amount itself. On the\n VC→VC path a fee below `1.0` additionally leaves at least one unit\n (`MaxPerPosition = sourceAmount - 1`), so that rounding alone can never make\n a small conversion impossible; `Fee = 1.0` is a deliberate ban and keeps its\n full effect. `netSource = sourceAmount - feeAmount`; `netSource <= 0` is\n rejected.\n9. **Output**: `output = netSource * rate`.\n - VC↔VC (`ConvertAsync`): `output` is cast straight to `long`, i.e.\n **truncated toward zero**. `output <= 0` after truncation is rejected\n (\"Resulting target amount is zero\").\n - Crypto-source (`ConvertCryptoAsync`): `output` stays a full-precision\n `decimal` if the target is `Crypto`. If the target is `Virtual`, it is\n **floored** (`Math.Floor`) to a `long` before the zero-check.\n10. Per-pair `DailyLimit` on the matched `ConversionTarget`: today's\n already-converted amount for this exact `(SourceType:SourceID ->\nTargetType:TargetID)` pair (tracked server-side per UTC day; not exposed\n to the client) plus this operation's raw source amount must not exceed\n it.\n11. The debit/credit itself runs through `ResourceService\n.ApplyResourceOperationAtomicAsync`, which additionally enforces the\n source currency's own `Economy.MinBalance`/`MaxBalance` (for VC) and\n `Economy.DailyEarnLimit`/`DailySpendLimit` — a **second, independent**\n limit layer scoped to the whole currency rather than this one pair. A\n crypto source's live balance is checked directly against\n `InventoryV2.CryptoCurrencies[id].Amount` before the debit.\n\n**Practical takeaway**: two conversions that look identical (same pair, same\namount) can differ in outcome depending on how much of the _daily_ pair\nallowance or the _daily_ currency-wide allowance is already used — always\nrender the server's `error` rather than trying to precompute eligibility.\n\n---\n\n## The shared resource primitives\n\nThis is the **canonical documentation** for these types — every other module\nskill (Store, Character, Craft, Lootbox, Blockchain, …) links here instead of\nredefining them. They describe \"spend this, receive that\" in one uniform\nshape used for offer costs, upgrade costs, craft inputs/outputs, lootbox\nprices/rewards, and blockchain deposit/withdrawal resource deltas.\n\n### ResourceEntry\n\nThe atomic unit: one currency or item quantity.\n\n```ts\ninterface ResourceEntry {\n Type?:\n | \"Item\"\n | \"VirtualCurrency\"\n | \"CryptoCurrency\"\n | \"Purchase\"\n | \"RewardedVideoCredit\"; // ResourceEntryType\n CurrencyID?: string; // set when Type is a currency kind\n Amount?: number; // integer amount; C# `long` on the wire, parsed via zVcAmount (exact up to 2^53-1)\n CatalogID?: string; // set when Type is \"Item\": which catalog\n ItemID?: string; // set when Type is \"Item\": which item definition\n ProductID?: string; // set when Type is \"Purchase\": the IAP product that pays for this entry\n}\n```\n\nOnly the fields relevant to `Type` are populated — e.g. a `VirtualCurrency`\nentry sets `CurrencyID` + `Amount` and leaves `CatalogID`/`ItemID` unset; an\n`Item` entry sets `CatalogID`/`ItemID` (+ `Amount` for stackable quantity) and\nleaves `CurrencyID` unset.\n\n### ResourceBundle\n\nA flat list of entries, plus optional event-token deltas:\n\n```ts\ninterface ResourceBundle {\n Entries?: ResourceEntry[] | null;\n EventTokens?: EventTokenOperation[] | null;\n}\n```\n\n### PremiumTierBundle\n\nAn alternate bundle that only applies if the player holds a qualifying\npremium tier — used inside `ResourceGrant`/`ResourceConsume` to express\n\"VIPs get a better grant / a cheaper cost.\"\n\n```ts\ninterface PremiumTierBundle {\n MinPremiumTier?: number;\n RequiredPremiumID?: string;\n Resources?: ResourceBundle | null;\n}\n```\n\n### ResourceGrant\n\nWhat a player receives.\n\n```ts\ninterface ResourceGrant {\n Standard?: ResourceBundle | null; // baseline grant, always applies\n PremiumBonuses?: unknown[] | null; // reserved/opaque bonus list\n PremiumTiers?: PremiumTierBundle[] | null; // tier-gated additional/alternate grants\n}\n```\n\n### ResourceConsume\n\nWhat a player is charged. Mirror-shaped to `ResourceGrant`, but the\ntier-based array is a **discount** mechanism rather than a bonus one — see\nGotchas below.\n\n```ts\ninterface ResourceConsume {\n Standard?: ResourceBundle | null; // baseline cost\n PremiumDiscounts?: unknown[] | null; // reserved/opaque discount list\n PremiumTiers?: PremiumTierBundle[] | null; // tier-gated reduced/alternate cost\n}\n```\n\n### ResourceOperation\n\nThe full manifest for one action: what's granted and what's consumed. This is\nthe shape every \"did an action succeed\" response embeds under a `Resources`\nfield (e.g. `DepositNFTResponse.Resources`, `NFTWithdrawalResponse.Resources`\nin Blockchain).\n\n```ts\ninterface ResourceOperation {\n Grant?: ResourceGrant | null;\n Consume?: ResourceConsume | null;\n}\n```\n\nEither side can be `null`/absent — a pure grant (no cost) sets only `Grant`;\na pure charge (no payout) sets only `Consume`.\n\n### EventTokenOperation / EventTokenAddress\n\nEvent tokens are a lighter-weight counter mechanic (e.g. season/event\ncurrency) addressed by an entity rather than a flat `CurrencyID`:\n\n```ts\ninterface EventTokenAddress {\n Type?: string; // EventTokenType — which kind of entity owns the token bucket\n EntityID: string;\n}\n\ninterface EventTokenOperation {\n Address?: EventTokenAddress | null;\n Amount?: number;\n Source?: string; // free-form provenance tag\n}\n```\n\n### ResourceDualPartyResult / ResourceTransferResult\n\nUsed by PvP/transfer-style features where two accounts are affected by one\naction:\n\n```ts\n// Each side gets its own independent grant/consume manifest.\ninterface ResourceDualPartyResult {\n FromUserID?: string;\n ToUserID?: string;\n FromResult?: ResourceOperation | null;\n ToResult?: ResourceOperation | null;\n}\n\n// A straight transfer: one bundle moves from one account to another.\ninterface ResourceTransferResult {\n FromUserID?: string;\n ToUserID?: string;\n Transferred?: ResourceBundle | null;\n}\n```\n\n---\n\n## How the SDK applies a ResourceOperation\n\nEvery module that returns a `ResourceOperation` (directly, or via a\n`Resources` field) has already had it **applied server-side**; the SDK's job\nis only to mirror it into the local cache so balances/inventory read\ncorrectly without a re-fetch. Internally this goes through\n`UserData.applyResourceOperation(op, itemDefs)`, which:\n\n- walks `Consume.Standard.Entries` and `Grant.Standard.Entries` (the\n `PremiumDiscounts`/`PremiumTiers`/`PremiumBonuses` arrays describe _why_ the\n standard amount is what it is — the server has already resolved them into\n `Standard` before sending the response; the client does not re-apply tiers),\n- for `VirtualCurrency` entries, adjusts the integer balance and emits\n `user:virtualCurrencyUpdated`,\n- for `Item` entries, adjusts stackable counts / creates unstackable instances\n and emits `user:inventoryUpdated`,\n- for `EventTokens`, adjusts the addressed token bucket and emits\n `user:eventTokenUpdated`,\n- always emits the umbrella `user:anyUpdated` when anything changed.\n\n`CryptoCurrency` amounts do **not** flow through this integer pipeline —\nthey're decimal and go through a separate patch\n(`UserData.patchCryptoCurrencyDelta(currencyID, delta, serverTimeUtc)`), which\nis what `CurrencyService.cryptoConvert` and the Blockchain deposit/withdrawal\nmethods use directly instead of embedding crypto deltas in a\n`ResourceOperation`.\n\n**Practical takeaway when building UI in any module:** don't hand-roll cost\npreviews from `PremiumDiscounts`/`PremiumTiers` internals unless you're\nexplicitly building a \"your VIP tier saves you N%\" comparison — for \"what will\nthis cost me right now,\" prefer the value the server already resolved\n(`Standard`, or the flat response fields like `ConvertResponse.SourceSpent`).\nTreat `Amount` on VC entries as a signed integer conceptually (consume vs.\ngrant is which container it's in, not a negative number) and crypto strings as\nopaque decimal values to hand to `decimal.js`, not to parse with `Number()`\nonce you're near precision limits.\n"
|
|
8
|
+
"content": "# Currency data model — reference\n\nFull shape of the `CurrencyDefinitions` config, and — the canonical\ndocumentation for the whole SDK — the shared `ResourceConsume` /\n`ResourceGrant` / `ResourceOperation` / `ResourceEntry` cost-and-reward\nprimitives. All types are **strictly typed** and exported from\n`@idosgames/core`; every object schema keeps `.passthrough()`, so a field the\nbackend adds later still round-trips instead of being stripped. Field names\nare PascalCase (straight from the backend JSON).\n\n## Contents\n\n- [Config: CurrencyDefinitions](#config-currencydefinitions)\n- [VirtualCurrencyDefinition](#virtualcurrencydefinition)\n- [CryptoCurrencyDefinition](#cryptocurrencydefinition)\n- [Shared conversion config](#shared-conversion-config)\n- [Backend conversion formulas](#backend-conversion-formulas) — transcribed from `ConversionService.cs`\n- [The shared resource primitives](#the-shared-resource-primitives) — canonical home\n - [ResourceEntry](#resourceentry)\n - [ResourceBundle](#resourcebundle)\n - [PremiumTierBundle](#premiumtierbundle)\n - [ResourceGrant](#resourcegrant)\n - [ResourceConsume](#resourceconsume)\n - [ResourceOperation](#resourceoperation)\n - [EventTokenOperation / EventTokenAddress](#eventtokenoperation--eventtokenaddress)\n - [ResourceDualPartyResult / ResourceTransferResult](#resourcedualpartyresult--resourcetransferresult)\n- [How the SDK applies a ResourceOperation](#how-the-sdk-applies-a-resourceoperation)\n\n---\n\n## Config: CurrencyDefinitions\n\n```ts\ninterface CurrencyDefinitions {\n VirtualCurrencies?: Record<string, VirtualCurrencyDefinition> | null;\n CryptoCurrencies?: Record<string, CryptoCurrencyDefinition> | null;\n CryptoRewardsFromDeveloperShare?: boolean; // top up unbacked crypto rewards from the developer share (off by default; otherwise the IOU is granted)\n CryptoIouForScriptsAndAI?: boolean; // CloudCode / AI rewards also fall back to the IOU instead of refusing (off by default)\n CryptoAutoRepayIouOnWithdrawal?: boolean; // repay IOUs on withdrawal from the players' reward pool, only the missing part (absent = on)\n}\n```\n\nKey in both maps is the `CurrencyID`. A currency is \"known\" iff it has an\nentry in one of these maps under its `CurrencyType` (`Virtual` or `Crypto`).\n\n---\n\n## VirtualCurrencyDefinition\n\n```ts\ninterface VirtualCurrencyDefinition {\n CurrencyID: string;\n DisplayName?: string;\n AssetPaths?: Record<string, string>; // \"icon\", ...\n Economy?: {\n ValueInUSD?: string; // decimal string\n ValueInUSDUpdatedAt?: string; // ISO timestamp\n InitialDeposit?: number; // starting balance for new players\n MinBalance?: number;\n MaxBalance?: number;\n DailyEarnLimit?: number;\n DailySpendLimit?: number;\n };\n Recharge?: {\n // Energy-style auto-regen, credited in BATCHES: every FULL `Period` seconds the\n // player gets `Rate` units at once, up to `Max`. Rate=5/Period=60 means \"+5 once a\n // minute\", NOT \"+1 every 12 seconds\" — an incomplete period credits nothing.\n // The batch is clipped exactly at `Max` (if less than Rate is missing, only the\n // remainder is credited); at or above `Max` nothing is credited.\n // `Max` is the auto-recharge cap only — explicit grants may exceed it, up to\n // Economy.MaxBalance.\n Rate?: number;\n Max?: number;\n Period?: number;\n };\n Conversion?: CurrencyConversion; // see below\n Permissions?: {\n IsTradable?: boolean;\n IsPurchasable?: boolean;\n IsRefundable?: boolean;\n };\n Audit?: { CreatedAt?: string; UpdatedAt?: string };\n Status?: \"Active\" | \"Hidden\" | \"Deprecated\" | \"Maintenance\";\n}\n```\n\n`Status` governs whether the currency is usable/visible; `\"Maintenance\"` /\n`\"Deprecated\"` currencies typically reject conversions server-side even if\n`Conversion.Enabled` is true.\n\n---\n\n## CryptoCurrencyDefinition\n\n```ts\ninterface CryptoCurrencyDefinition {\n CurrencyID: string;\n DisplayName?: string;\n AssetPaths?: Record<string, string>;\n DisplayDecimals?: number; // UI rounding, not wire precision\n ValueInUSD?: string; // decimal string\n ValueInUSDUpdatedAt?: string;\n DeveloperWithdrawalFeePercent?: string; // decimal string; fee on PLAYER withdrawal, see blockchain-system\n CommunityMarketingWithdrawalFeePercent?: string; // decimal string; fee on PLAYER withdrawal\n UnbackedRewardCurrencyID?: string; // IOU virtual currency: unbacked part of a crypto reward arrives in it 1:1; spent first when this crypto is spent\n Networks?: CryptoNetworkBinding[]; // per-chain bindings\n Limits?: {\n DailyWithdrawUsd?: string;\n MonthlyWithdrawUsd?: string;\n KycRequiredAboveUsd?: string;\n };\n Permissions?: {\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n SpendableInGame?: boolean;\n ConvertibleToVirtual?: boolean; // gates cryptoConvert eligibility\n };\n Conversion?: CurrencyConversion;\n Audit?: { CreatedAt?: string; UpdatedAt?: string };\n Status?: \"Active\" | \"Hidden\" | \"Deprecated\" | \"Maintenance\";\n}\n\ninterface CryptoNetworkBinding {\n NetworkID: string;\n ContractAddress?: string;\n Decimals?: number; // on-chain token decimals\n MinWithdraw?: string;\n DepositsEnabled?: boolean;\n WithdrawalsEnabled?: boolean;\n}\n```\n\nDeposit/withdrawal flows (and `Networks`/`Limits` enforcement for those flows)\nbelong to the Blockchain module — see the blockchain-system skill.\n`Permissions.ConvertibleToVirtual` is the flag most relevant here: it's what\nlets a crypto balance participate in `cryptoConvert`.\n\n---\n\n## Shared conversion config\n\nBoth currency kinds reuse the same `CurrencyConversion` shape for their\n`Conversion` field:\n\n```ts\ninterface CurrencyConversion {\n Enabled?: boolean;\n RateMode?: \"Automatic\" | \"Manual\";\n Fee?: number; // SHARE of the debited amount: 0.05 = 5% (never 0..100)\n Targets?: ConversionTarget[]; // whitelist of valid conversion targets\n}\n\ninterface ConversionTarget {\n TargetCurrencyType?: \"Virtual\" | \"Crypto\";\n TargetCurrencyID: string;\n Rate?: string; // decimal string; used when RateMode is \"Manual\"\n MinAmount?: number;\n MaxAmount?: number;\n DailyLimit?: number;\n}\n```\n\nA conversion is only accepted if the source currency's `Conversion.Enabled`\nis true and the target appears in `Targets` (by type + id). `\"Automatic\"`\nrate mode means the backend derives the rate from each side's `ValueInUSD`;\n`\"Manual\"` uses the `Rate` pinned on the `ConversionTarget`. Either way, treat\n`RateApplied` on the response as the source of truth — don't recompute it.\n\n---\n\n## Backend conversion formulas\n\nTranscribed from `ConversionService.ConvertAsync` /\n`ConversionService.ConvertCryptoAsync` in the backend\n(`IDosGamesSDK/API/Client/v2/Currency/Services/ConversionService.cs`). These\nare enforced server-side; the SDK never recomputes them — use this section\nonly for building accurate cost/reward **previews**, not for validating a\nconversion before sending it.\n\n**Order of checks** (any failure short-circuits, no partial debit):\n\n1. Basic shape: non-empty `SourceID`/`TargetID`, positive amount, source ≠\n target.\n2. `Convert` (VC↔VC) rejects a `Crypto` source outright (\"Convert supports\n VC↔VC only\") and a `Crypto` target outright (\"Virtual→Crypto conversion is\n not supported\"). `CryptoConvert` rejects a non-`Crypto` source outright\n (\"CryptoConvert requires source to be Crypto\").\n3. **Status**: source or target `Maintenance` → rejected on that side (\"is\n under maintenance\"). Target (only) `Deprecated` → rejected (\"is deprecated\n and cannot receive new credits\"). A `Deprecated` **source** is allowed —\n deprecating a currency only stops new inflow, it doesn't trap the player's\n remaining balance.\n4. Source's `Conversion` must be non-null and `Enabled`, and must have a\n `Targets` entry matching `(TargetCurrencyType, TargetCurrencyID)` exactly —\n otherwise \"Conversion from 'X' to 'Y' is not allowed.\"\n5. Crypto-source → Virtual-target additionally requires\n `CryptoCurrencyPermissions.ConvertibleToVirtual` — false rejects even a\n listed target.\n6. Per-pair `MinAmount`/`MaxAmount` on the matched `ConversionTarget`, checked\n against the raw source amount before fee.\n7. **Rate resolution**:\n - `RateMode = Manual` → `rate = ConversionTarget.Rate`; a pair configured\n Manual with no `Rate` set is rejected (\"Manual conversion rate is not set\n for this pair\"), not treated as 0 or 1.\n - `RateMode = Automatic` → `rate = source.ValueInUSD / target.ValueInUSD`.\n Either side missing/zero `ValueInUSD` rejects the conversion (\"Automatic\n rate cannot be computed\").\n8. **Fee**: `Fee` is a SHARE (`0.05` = 5%), so `feeAmount = sourceAmount * Fee`\n — there is no division by 100. It is **not** clamped to a range: only `NaN`\n is treated as zero, and the fee is capped at the source amount itself. On the\n VC→VC path a fee below `1.0` additionally leaves at least one unit\n (`MaxPerPosition = sourceAmount - 1`), so that rounding alone can never make\n a small conversion impossible; `Fee = 1.0` is a deliberate ban and keeps its\n full effect. `netSource = sourceAmount - feeAmount`; `netSource <= 0` is\n rejected.\n9. **Output**: `output = netSource * rate`.\n - VC↔VC (`ConvertAsync`): `output` is cast straight to `long`, i.e.\n **truncated toward zero**. `output <= 0` after truncation is rejected\n (\"Resulting target amount is zero\").\n - Crypto-source (`ConvertCryptoAsync`): `output` stays a full-precision\n `decimal` if the target is `Crypto`. If the target is `Virtual`, it is\n **floored** (`Math.Floor`) to a `long` before the zero-check.\n10. Per-pair `DailyLimit` on the matched `ConversionTarget`: today's\n already-converted amount for this exact `(SourceType:SourceID ->\nTargetType:TargetID)` pair (tracked server-side per UTC day; not exposed\n to the client) plus this operation's raw source amount must not exceed\n it.\n11. The debit/credit itself runs through `ResourceService\n.ApplyResourceOperationAtomicAsync`, which additionally enforces the\n source currency's own `Economy.MinBalance`/`MaxBalance` (for VC) and\n `Economy.DailyEarnLimit`/`DailySpendLimit` — a **second, independent**\n limit layer scoped to the whole currency rather than this one pair. A\n crypto source's live balance is checked directly against\n `InventoryV2.CryptoCurrencies[id].Amount` before the debit.\n\n**Practical takeaway**: two conversions that look identical (same pair, same\namount) can differ in outcome depending on how much of the _daily_ pair\nallowance or the _daily_ currency-wide allowance is already used — always\nrender the server's `error` rather than trying to precompute eligibility.\n\n---\n\n## The shared resource primitives\n\nThis is the **canonical documentation** for these types — every other module\nskill (Store, Character, Craft, Lootbox, Blockchain, …) links here instead of\nredefining them. They describe \"spend this, receive that\" in one uniform\nshape used for offer costs, upgrade costs, craft inputs/outputs, lootbox\nprices/rewards, and blockchain deposit/withdrawal resource deltas.\n\n### ResourceEntry\n\nThe atomic unit: one currency or item quantity.\n\n```ts\ninterface ResourceEntry {\n Type?:\n | \"Item\"\n | \"VirtualCurrency\"\n | \"CryptoCurrency\"\n | \"Purchase\"\n | \"RewardedVideoCredit\"; // ResourceEntryType\n CurrencyID?: string; // set when Type is a currency kind\n Amount?: number; // integer amount; C# `long` on the wire, parsed via zVcAmount (exact up to 2^53-1)\n CatalogID?: string; // set when Type is \"Item\": which catalog\n ItemID?: string; // set when Type is \"Item\": which item definition\n ProductID?: string; // set when Type is \"Purchase\": the IAP product that pays for this entry\n}\n```\n\nOnly the fields relevant to `Type` are populated — e.g. a `VirtualCurrency`\nentry sets `CurrencyID` + `Amount` and leaves `CatalogID`/`ItemID` unset; an\n`Item` entry sets `CatalogID`/`ItemID` (+ `Amount` for stackable quantity) and\nleaves `CurrencyID` unset.\n\n### ResourceBundle\n\nA flat list of entries, plus optional event-token deltas:\n\n```ts\ninterface ResourceBundle {\n Entries?: ResourceEntry[] | null;\n EventTokens?: EventTokenOperation[] | null;\n}\n```\n\n### PremiumTierBundle\n\nAn alternate bundle that only applies if the player holds a qualifying\npremium tier — used inside `ResourceGrant`/`ResourceConsume` to express\n\"VIPs get a better grant / a cheaper cost.\"\n\n```ts\ninterface PremiumTierBundle {\n MinPremiumTier?: number;\n RequiredPremiumID?: string;\n Resources?: ResourceBundle | null;\n}\n```\n\n### ResourceGrant\n\nWhat a player receives.\n\n```ts\ninterface ResourceGrant {\n Standard?: ResourceBundle | null; // baseline grant, always applies\n PremiumBonuses?: unknown[] | null; // reserved/opaque bonus list\n PremiumTiers?: PremiumTierBundle[] | null; // tier-gated additional/alternate grants\n}\n```\n\n### ResourceConsume\n\nWhat a player is charged. Mirror-shaped to `ResourceGrant`, but the\ntier-based array is a **discount** mechanism rather than a bonus one — see\nGotchas below.\n\n```ts\ninterface ResourceConsume {\n Standard?: ResourceBundle | null; // baseline cost\n PremiumDiscounts?: unknown[] | null; // reserved/opaque discount list\n PremiumTiers?: PremiumTierBundle[] | null; // tier-gated reduced/alternate cost\n}\n```\n\n### ResourceOperation\n\nThe full manifest for one action: what's granted and what's consumed. This is\nthe shape every \"did an action succeed\" response embeds under a `Resources`\nfield (e.g. `DepositNFTResponse.Resources`, `NFTWithdrawalResponse.Resources`\nin Blockchain).\n\n```ts\ninterface ResourceOperation {\n Grant?: ResourceGrant | null;\n Consume?: ResourceConsume | null;\n}\n```\n\nEither side can be `null`/absent — a pure grant (no cost) sets only `Grant`;\na pure charge (no payout) sets only `Consume`.\n\n### EventTokenOperation / EventTokenAddress\n\nEvent tokens are a lighter-weight counter mechanic (e.g. season/event\ncurrency) addressed by an entity rather than a flat `CurrencyID`:\n\n```ts\ninterface EventTokenAddress {\n Type?: string; // EventTokenType — which kind of entity owns the token bucket\n EntityID: string;\n}\n\ninterface EventTokenOperation {\n Address?: EventTokenAddress | null;\n Amount?: number;\n Source?: string; // free-form provenance tag\n}\n```\n\n### ResourceDualPartyResult / ResourceTransferResult\n\nUsed by PvP/transfer-style features where two accounts are affected by one\naction:\n\n```ts\n// Each side gets its own independent grant/consume manifest.\ninterface ResourceDualPartyResult {\n FromUserID?: string;\n ToUserID?: string;\n FromResult?: ResourceOperation | null;\n ToResult?: ResourceOperation | null;\n}\n\n// A straight transfer: one bundle moves from one account to another.\ninterface ResourceTransferResult {\n FromUserID?: string;\n ToUserID?: string;\n Transferred?: ResourceBundle | null;\n}\n```\n\n---\n\n## How the SDK applies a ResourceOperation\n\nEvery module that returns a `ResourceOperation` (directly, or via a\n`Resources` field) has already had it **applied server-side**; the SDK's job\nis only to mirror it into the local cache so balances/inventory read\ncorrectly without a re-fetch. Internally this goes through\n`UserData.applyResourceOperation(op, itemDefs)`, which:\n\n- walks `Consume.Standard.Entries` and `Grant.Standard.Entries` (the\n `PremiumDiscounts`/`PremiumTiers`/`PremiumBonuses` arrays describe _why_ the\n standard amount is what it is — the server has already resolved them into\n `Standard` before sending the response; the client does not re-apply tiers),\n- for `VirtualCurrency` entries, adjusts the integer balance and emits\n `user:virtualCurrencyUpdated`,\n- for `Item` entries, adjusts stackable counts / creates unstackable instances\n and emits `user:inventoryUpdated`,\n- for `EventTokens`, adjusts the addressed token bucket and emits\n `user:eventTokenUpdated`,\n- always emits the umbrella `user:anyUpdated` when anything changed.\n\n`CryptoCurrency` amounts do **not** flow through this integer pipeline —\nthey're decimal and go through a separate patch\n(`UserData.patchCryptoCurrencyDelta(currencyID, delta, serverTimeUtc)`), which\nis what `CurrencyService.cryptoConvert` and the Blockchain deposit/withdrawal\nmethods use directly instead of embedding crypto deltas in a\n`ResourceOperation`.\n\n**Practical takeaway when building UI in any module:** don't hand-roll cost\npreviews from `PremiumDiscounts`/`PremiumTiers` internals unless you're\nexplicitly building a \"your VIP tier saves you N%\" comparison — for \"what will\nthis cost me right now,\" prefer the value the server already resolved\n(`Standard`, or the flat response fields like `ConvertResponse.SourceSpent`).\nTreat `Amount` on VC entries as a signed integer conceptually (consume vs.\ngrant is which container it's in, not a negative number) and crypto strings as\nopaque decimal values to hand to `decimal.js`, not to parse with `Number()`\nonce you're near precision limits.\n"
|
|
9
9
|
}
|
|
10
10
|
]
|
|
11
11
|
}
|
|
@@ -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, …). 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) and \"Play\" launches a game on the whole screen with a small button back\n — or, for a game whose route says `inLobby: true` (idle-rpg), straight into that game inside the\n lobby, the lobby's balances and tabs around it. 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) and \"Play\" launches a game on the whole screen with a small button back\n — or, for a game whose route says `inLobby: true` (idle-rpg), straight into that game inside the\n lobby, the lobby's balances and tabs around it. 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`, `kanban:read`, `kanban:write`, `build:write`. 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 **Shipping the game itself** (permission `build:write`) also goes through this server — the\n platform hosts it on the Title's own address (`{titleid}.idos.games`) and its idosgames.com page:\n\n 1. `begin_build_upload { title_id }` → `build_id`, a one-hour `upload_url`, `asset_base`.\n 2. Build the game as static files with that base: `vite build --base=<asset_base>` (a relative\n `./` base also works — Unity WebGL builds already are). Use the LIVE title id, never `-DEV`.\n 3. Zip the CONTENTS of the output folder (`index.html` at the zip root; a single `dist/` top folder\n is accepted) and upload it: `curl -T build.zip \"<upload_url>\"` — a plain PUT, no headers.\n 4. `finish_build_upload { title_id, build_id, deploy }` — unpacks it into a new version (it shows up\n in the dashboard's version history) and returns `TestUrl` (DEV address). `deploy: true` puts it\n live for players right away; without it, check `TestUrl` and call it again with `deploy: true`.\n Still unpacking when it returns → poll `get_build`. `list_builds` shows recent versions.\n\n A game that is live but whose Title is not published yet has no idosgames.com page — the\n response says so; the publisher publishes it from the dashboard.\n\n ⚠ **The build must be the game itself.** On the Title's idosgames.com page the game may embed\n (`<iframe>`) only platform hosts — never another website, with no setting to allow one. A\n \"wrapper\" build that signs in and then frames the publisher's own site shows **\"Content\n unavailable\"** there instead of the game: build and upload the real game. Network calls\n (`fetch`/WebSocket) from the embedded game reach platform hosts plus the Title's\n `Blockchain.IframeConnectHosts` (`save_blockchain`; wallet-connection hosts are always refused).\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
|
}
|