{"openapi":"3.1.0","info":{"title":"Chia Gaming Tracker","version":"0.1.0","license":{"name":"MIT"},"description":"Chia blockchain gaming registry. Publishes game manifests (content-addressed by\n`gamePuzzleHash`) and tracks state-channel rooms. SQLite-backed, strict validation,\nBLS-signed mutations.\n\n## Two signature variants\n\n| Variant           | Algorithm                                                                          | Used by                                                                                                |\n| ----------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |\n| **CHIP-0002**     | `treeHash(cons(atom(\"Chia Signed Message\"), atom(message)))` — Goby / Sage wallets | `POST /games`, `PUT /games/:id`, `DELETE /games/:id`, `POST /rooms`, `POST /rooms/:id/join`            |\n| **Direct BLS**    | BLS sign/verify over sha256 of raw UTF-8 message bytes — ephemeral identity keys   | `PATCH /rooms/:id`, `POST /rooms/:id/results`, `DELETE /rooms/:id`                                     |\n\nSee the `walletSignature` and `identitySignature` security schemes for the canonical\nmessage templates. The server reconstructs the canonical form and byte-compares\nagainst `signature.message`; any difference returns `SIGNATURE_MESSAGE_MISMATCH`.\n\n## Signature location\n\n- `POST /games`, `PUT /games/:id` — signature is embedded at `manifest.signature`\n  (the author's public key is at `manifest.author.publicKey`). No top-level\n  `body.signature`.\n- `DELETE /games/:id` — top-level `body.signature` with `publicKey`.\n- All room endpoints — top-level `body.signature` with `publicKey`.\n\n## Multi-author (contract vs UI)\n\n`gamePuzzleHash` is unique per author (`UNIQUE(gamePuzzleHash, authorPublicKey)`).\nThe first publisher of a hash becomes the contract author (`isContractAuthor: true`).\nSubsequent publishers of the same hash with a different key become UI authors\n(`isContractAuthor: false`). Use `GET /games/by-puzzle-hash/:hash` to retrieve both.\n\n## Rate limiting\n\nRead endpoints are limited to `RATE_LIMIT_MAX_READS` (default 120) requests per IP per\nwindow, writes to `RATE_LIMIT_MAX_WRITES` (default 20). Exceeding returns `429` with\nerror code `RATE_LIMITED`.\n"},"servers":[{"url":"http://localhost:8766","description":"Local dev server (`npm run dev`)"}],"tags":[{"name":"health","description":"Liveness and stats."},{"name":"games","description":"Game manifest registry."},{"name":"rooms","description":"State-channel room tracking."}],"paths":{"/health":{"get":{"tags":["health"],"summary":"Liveness probe","operationId":"getHealth","responses":{"200":{"description":"Service is up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"status":"ok","timestamp":1760000000000}}}}},"x-code-samples":[{"lang":"shell","label":"curl","source":"curl http://localhost:8766/health"}]}},"/stats":{"get":{"tags":["health"],"summary":"Aggregate tracker stats (cached 10s)","operationId":"getStats","responses":{"200":{"description":"Snapshot of room/game counts.","headers":{"Cache-Control":{"schema":{"type":"string"},"example":"public, max-age=10"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsSnapshot"}}}}},"x-code-samples":[{"lang":"shell","label":"curl","source":"curl http://localhost:8766/stats"}]}},"/games":{"get":{"tags":["games"],"summary":"List games","operationId":"listGames","parameters":[{"name":"author","in":"query","schema":{"type":"string"},"description":"Filter by author BLS public key (96-hex)."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Substring match on name/description."},{"name":"gameType","in":"query","schema":{"type":"string"},"description":"Filter by gameType (even-length lowercase hex)."},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","oldest","name"]}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameListResult"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-code-samples":[{"lang":"shell","label":"curl","source":"curl 'http://localhost:8766/games?limit=20&sort=newest'"}]},"post":{"tags":["games"],"summary":"Publish a new game manifest","operationId":"publishGame","description":"Signature lives at `manifest.signature`. Author's public key is at\n`manifest.author.publicKey`. Canonical message (newline-separated, lowercase\nhex, no `0x`):\n\n```\npublish\ngamePuzzleHash:<64-hex>\ngameType:<utf8-hex>\ngameUrl:<absolute-url>                OR  packageHash:<64-hex>  (file mode)\ntimestamp:<unix-seconds>\n```\n\nThe server fetches `package.url` and verifies the checksum (file mode) or the\nadvertised `gameUrl` CORS + availability (url mode). Publishing the same\n`gamePuzzleHash` as a different author produces a UI author record\n(`isContractAuthor: false`). Same author + same hash is rejected — use `PUT` to\nupdate.\n","security":[{"walletSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishGameRequest"},"example":{"manifest":{"manifestVersion":1,"id":"my-game","name":"My Game","version":"1.0.0","description":"An example game.","gamePuzzleHash":"5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","gameType":"6d792d67616d65","gameUrl":"http://127.0.0.1:3000/my-game/","clsp":{"proposal":"/clsp/proposal.hex","parser":"/clsp/parser.hex"},"author":{"name":"Alice","publicKey":"9769020abfcda5a0b8b9d742a94ee2ab09849a9e97cb22e01cab92c5f98f06cb795e7e49d6c5ad4d00b5b3e0ae33f30a"},"signature":{"message":"publish\ngamePuzzleHash:5feceb66...\ngameType:6d792d67616d65\ngameUrl:http://127.0.0.1:3000/my-game/\ntimestamp:1760000000","sig":"<192-hex>","timestamp":1760000000}},"package":{"type":"url","url":"http://127.0.0.1:3000/my-game/"}}}}},"responses":{"201":{"description":"Game published.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRecord"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-code-samples":[{"lang":"shell","label":"curl","source":"curl -X POST http://localhost:8766/games \\\n  -H 'content-type: application/json' \\\n  --data-binary @manifest.json\n"}]}},"/games/{gameId}":{"parameters":[{"name":"gameId","in":"path","required":true,"schema":{"type":"string","maxLength":50,"pattern":"^[a-zA-Z0-9_-]+$"}}],"get":{"tags":["games"],"summary":"Get a game by id","operationId":"getGame","responses":{"200":{"description":"Game record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRecord"}}}},"404":{"$ref":"#/components/responses/NotFound"}},"x-code-samples":[{"lang":"shell","label":"curl","source":"curl http://localhost:8766/games/my-game"}]},"put":{"tags":["games"],"summary":"Update an existing game (owner only)","operationId":"updateGame","description":"Only the original author (matched by `manifest.author.publicKey`) can update.\nSignature canonical message uses action `update` (same template as publish).\n","security":[{"walletSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishGameRequest"}}}},"responses":{"200":{"description":"Game updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRecord"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["games"],"summary":"Delete (deactivate) a game (owner only)","operationId":"deleteGame","description":"Signature is at top-level `body.signature`. Canonical message uses action\n`delete`. Deactivates the record (`active: false`); it stops appearing in\nlistings but the row is retained.\n","security":[{"walletSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteGameRequest"}}}},"responses":{"200":{"description":"Deletion acknowledged.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/games/by-puzzle-hash/{hash}":{"get":{"tags":["games"],"summary":"Look up all publishers of a puzzle hash","operationId":"getGamesByPuzzleHash","description":"Returns the contract author (first publisher, `isContractAuthor: true`) plus\nevery UI author that published the same `gamePuzzleHash` with a different key.\nConsumed by `chia-gaming-connect` via `TrackerService.lookupByFactoryHash()`.\n","parameters":[{"name":"hash","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Hash64"}}],"responses":{"200":{"description":"Contract author + UI authors.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ByPuzzleHashResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"404":{"$ref":"#/components/responses/NotFound"}},"x-code-samples":[{"lang":"shell","label":"curl","source":"curl http://localhost:8766/games/by-puzzle-hash/5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\n"}]}},"/rooms":{"get":{"tags":["rooms"],"summary":"List rooms","operationId":"listRooms","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["waiting","active","playing","closed","all"]}},{"name":"gameType","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"minWager","in":"query","schema":{"type":"integer","minimum":0}},{"name":"maxWager","in":"query","schema":{"type":"integer","minimum":0}},{"name":"includePrivate","in":"query","schema":{"type":"boolean"}},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","oldest","wager_high","wager_low"]}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomListResult"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["rooms"],"summary":"Create a room (P1 wallet signs)","operationId":"createRoom","description":"Canonical CHIP-0002 message:\n\n```\nannounce:create\nroomId:<id>\nplayer1Wallet:<chia-address>\nplayer1IdentityPubKey:<bls-g1-hex>\ntimestamp:<unix-seconds>\n```\n","security":[{"walletSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoomRequest"}}}},"responses":{"201":{"description":"Room created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomRecord"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/rooms/{roomId}":{"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9_-]+$"}}],"get":{"tags":["rooms"],"summary":"Get a room by id","operationId":"getRoom","responses":{"200":{"description":"Room record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomRecord"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["rooms"],"summary":"Update room state (P1 or P2 identity signs)","operationId":"patchRoom","description":"Uses **Direct BLS** (not CHIP-0002). Canonical message:\n\n```\nannounce:update\nroomId:<id>\ntimestamp:<unix-seconds>\n```\n\nThe `signature.publicKey` must match either `player1.identityPubKey` or\n`player2.identityPubKey` of the room.\n","security":[{"identitySignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchRoomRequest"}}}},"responses":{"200":{"description":"Updated room.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomRecord"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["rooms"],"summary":"Close a room (P1 or P2 identity signs)","operationId":"closeRoom","description":"Direct BLS signature. Canonical message:\n\n```\nannounce:close\nroomId:<id>\ntimestamp:<unix-seconds>\n```\n","security":[{"identitySignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloseRoomRequest"}}}},"responses":{"200":{"description":"Room closed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/rooms/{roomId}/join":{"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string","maxLength":100}}],"post":{"tags":["rooms"],"summary":"Join a waiting room (P2 wallet signs)","operationId":"joinRoom","description":"Canonical CHIP-0002 message:\n\n```\nannounce:join\nroomId:<id>\nplayer2Wallet:<chia-address>\nplayer2IdentityPubKey:<bls-g1-hex>\ntimestamp:<unix-seconds>\n```\n","security":[{"walletSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinRoomRequest"}}}},"responses":{"200":{"description":"Joined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomRecord"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/rooms/{roomId}/results":{"parameters":[{"name":"roomId","in":"path","required":true,"schema":{"type":"string","maxLength":100}}],"get":{"tags":["rooms"],"summary":"List finished game results in a room","operationId":"listRoomResults","responses":{"200":{"description":"List of per-game results.","content":{"application/json":{"schema":{"type":"object","properties":{"roomId":{"type":"string"},"results":{"type":"array","items":{"$ref":"#/components/schemas/RoomGameRecord"}}},"required":["roomId","results"]}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["rooms"],"summary":"Report a finished game (identity signs)","operationId":"reportRoomResult","description":"Direct BLS signature. Canonical message:\n\n```\nannounce:result\nroomId:<id>\ngameId:<id>\ntimestamp:<unix-seconds>\n```\n","security":[{"identitySignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResultRequest"}}}},"responses":{"201":{"description":"Result stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomGameRecord"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}}},"components":{"securitySchemes":{"walletSignature":{"type":"apiKey","in":"header","name":"X-Wallet-Signature-Location","description":"**Not a real HTTP header.** OpenAPI cannot express \"signature in request body\" —\nthis scheme documents the actual mechanism.\n\n**Algorithm (CHIP-0002):**\n`sig = BLS.sign(sk, sha256(treeHash(cons(atom(\"Chia Signed Message\"), atom(message)))))`\n— i.e. the CLVM tree-hash of a cons-pair of two atoms. Compatible with\nGoby/Sage wallet `chia_signMessage` methods.\n\n**Signature location:**\n- `POST /games`, `PUT /games/:id` → inside the body at `manifest.signature`.\n  Author's public key is at `manifest.author.publicKey`.\n- `DELETE /games/:id`, `POST /rooms`, `POST /rooms/:id/join` → top-level\n  `body.signature` with explicit `publicKey`.\n\n**Signature shape** (see `ManifestSignature` / `RequestSignature`):\n```json\n{ \"message\": \"...canonical multiline text...\", \"sig\": \"<192-hex>\", \"timestamp\": 1760000000 }\n```\nDELETE/rooms additionally include `publicKey: \"<96-hex>\"`.\n\n**Timestamp tolerance:** `SIGNATURE_TIMESTAMP_TOLERANCE_SEC` (default 300s).\nBeyond that, the server returns `SIGNATURE_TIMESTAMP_EXPIRED`.\n\n**Canonical message drift:** the server rebuilds the expected message from the\nparsed body and byte-compares against `signature.message`. Any difference\n(whitespace, ordering, case) returns `SIGNATURE_MESSAGE_MISMATCH`.\n"},"identitySignature":{"type":"apiKey","in":"header","name":"X-Identity-Signature-Location","description":"**Not a real HTTP header.** Documents the actual mechanism — signature in body.\n\n**Algorithm (Direct BLS):**\n`sig = BLS.sign(sk, sha256(utf8Bytes(message)))` — raw BLS sign/verify over\nsha256 of the UTF-8 message bytes. Used for ephemeral identity keys that never\ntouch a wallet.\n\n**Signature location:** top-level `body.signature` with `publicKey`:\n```json\n{\n  \"publicKey\": \"<96-hex>\",\n  \"message\": \"...canonical multiline text...\",\n  \"sig\": \"<192-hex>\",\n  \"timestamp\": 1760000000\n}\n```\n\n**Authorization:** `publicKey` must match `player1.identityPubKey` or\n`player2.identityPubKey` of the room (as set during `POST /rooms` and\n`POST /rooms/:id/join`).\n\n**Timestamp tolerance:** `SIGNATURE_TIMESTAMP_TOLERANCE_SEC` (default 300s).\n"}},"responses":{"ValidationError":{"description":"One or more request fields failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Signature missing, malformed, or verification failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Signer is not authorized for this action (wrong owner or wrong identity).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Resource conflict (duplicate id, duplicate `gamePuzzleHash` for same author, invalid state transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Per-IP rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Hash64":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"64-char lowercase hex (sha256 / puzzle hash).","example":"5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9"},"BlsPublicKey":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{96}$","description":"BLS G1 public key, 96 hex chars (0x prefix optional).","example":"9769020abfcda5a0b8b9d742a94ee2ab09849a9e97cb22e01cab92c5f98f06cb795e7e49d6c5ad4d00b5b3e0ae33f30a"},"BlsSignature":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{192}$","description":"BLS G2 signature, 192 hex chars (0x prefix optional)."},"Semver":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$","example":"1.0.0"},"Slug":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$","maxLength":100},"ChiaAddress":{"type":"string","pattern":"^t?xch1[a-z0-9]{20,100}$","description":"Bech32m Chia address (mainnet `xch1...` or testnet `txch1...`)."},"HealthResponse":{"type":"object","required":["status","timestamp"],"properties":{"status":{"type":"string","enum":["ok"]},"timestamp":{"type":"integer","description":"unix millis"}}},"StatsSnapshot":{"type":"object","required":["totalRooms","totalGames","rooms","byGameType","generatedAt"],"properties":{"totalRooms":{"type":"integer"},"totalGames":{"type":"integer","description":"Active games only."},"rooms":{"type":"object","required":["waiting","active","playing","closed"],"properties":{"waiting":{"type":"integer"},"active":{"type":"integer"},"playing":{"type":"integer"},"closed":{"type":"integer"}}},"byGameType":{"type":"array","items":{"type":"object","required":["gameType","count"],"properties":{"gameType":{"type":"string"},"count":{"type":"integer"}}}},"generatedAt":{"type":"integer","description":"unix millis of snapshot computation"}}},"Error":{"type":"object","required":["code","message","errors"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ErrorDetail"}}}},"ErrorDetail":{"type":"object","required":["code","message"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"path":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true}}},"ErrorCode":{"type":"string","enum":["MANIFEST_MISSING_FIELD","MANIFEST_FIELD_FORMAT","NAME_LOOKS_LIKE_HASH","SIGNATURE_MESSAGE_MISMATCH","SIGNATURE_MISMATCH","SIGNATURE_TIMESTAMP_EXPIRED","FETCH_FAILED","BAD_CONTENT_TYPE","HASH_MISMATCH","CORS_HEADER_MISSING","DUPLICATE_PUZZLEHASH_DIFFERENT_AUTHOR","DUPLICATE_ID","NOT_FOUND","FORBIDDEN","UNAUTHORIZED","RATE_LIMITED","INVALID_STATE","INTERNAL"]},"GameAuthor":{"type":"object","required":["name","publicKey"],"properties":{"name":{"type":"string","maxLength":50},"publicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"url":{"type":"string","format":"uri"}}},"GameClsp":{"type":"object","required":["proposal"],"properties":{"proposal":{"type":"string","description":"Path to a `.hex` file, relative to `gameUrl`."},"parser":{"type":"string"}}},"ManifestSignature":{"type":"object","required":["message","sig","timestamp"],"properties":{"message":{"type":"string","description":"Canonical message (see `walletSignature` description)."},"sig":{"$ref":"#/components/schemas/BlsSignature"},"timestamp":{"type":"integer","description":"unix seconds"}}},"GameManifest":{"type":"object","required":["manifestVersion","id","name","version","description","gamePuzzleHash","gameType","gameUrl","clsp","author","signature"],"properties":{"manifestVersion":{"type":"integer","minimum":1},"id":{"$ref":"#/components/schemas/Slug"},"name":{"type":"string","maxLength":50},"version":{"$ref":"#/components/schemas/Semver"},"description":{"type":"string","maxLength":300},"gamePuzzleHash":{"$ref":"#/components/schemas/Hash64"},"gameType":{"type":"string","pattern":"^[0-9a-f]+$","maxLength":256,"description":"Even-length lowercase hex."},"gameUrl":{"type":"string","format":"uri"},"clsp":{"$ref":"#/components/schemas/GameClsp"},"author":{"$ref":"#/components/schemas/GameAuthor"},"signature":{"$ref":"#/components/schemas/ManifestSignature"},"factoryVersion":{"type":"integer"},"sdkVersion":{"type":"string","maxLength":20},"icon":{"type":"string","description":"Inline SVG or data URL (<=10KB)."},"instructions":{"type":"string"},"license":{"type":"string"},"homepage":{"type":"string","format":"uri"},"repository":{"type":"string","format":"uri"},"downloadUrl":{"type":"string","format":"uri"}}},"GamePackage":{"type":"object","required":["type","url"],"properties":{"type":{"type":"string","enum":["url","file"]},"url":{"type":"string","format":"uri"},"size":{"type":"integer","minimum":0,"maximum":52428800,"description":"Required for `file` mode; max 50MB."},"checksum":{"type":"object","description":"Required for `file` mode.","required":["algorithm","hash"],"properties":{"algorithm":{"type":"string","enum":["sha256"]},"hash":{"$ref":"#/components/schemas/Hash64"}}}}},"GameRecord":{"type":"object","required":["id","gamePuzzleHash","gameType","name","version","authorPublicKey","gameUrl","manifest","package","isContractAuthor","createdAt","updatedAt","active"],"properties":{"id":{"$ref":"#/components/schemas/Slug"},"gamePuzzleHash":{"$ref":"#/components/schemas/Hash64"},"gameType":{"type":"string","pattern":"^[0-9a-f]+$"},"name":{"type":"string"},"version":{"$ref":"#/components/schemas/Semver"},"authorPublicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"gameUrl":{"type":"string","format":"uri"},"manifest":{"$ref":"#/components/schemas/GameManifest"},"package":{"$ref":"#/components/schemas/GamePackage"},"isContractAuthor":{"type":"boolean","description":"True for the first publisher of this `gamePuzzleHash`; false for subsequent UI authors."},"createdAt":{"type":"integer","description":"unix millis"},"updatedAt":{"type":"integer","description":"unix millis"},"active":{"type":"boolean"}}},"GameListResult":{"type":"object","required":["total","offset","limit","games"],"properties":{"total":{"type":"integer"},"offset":{"type":"integer"},"limit":{"type":"integer"},"games":{"type":"array","items":{"$ref":"#/components/schemas/GameRecord"}}}},"ByPuzzleHashResponse":{"type":"object","required":["contractAuthor","uis"],"description":"Primary + alternatives shape. `contractAuthor` is the first/canonical publisher\nof this puzzle hash; `uis` is the list of *additional* UI authors (different\npublic keys, same hash). The contract author is never repeated inside `uis`.\nTo build a flat \"all UIs\" list, use `[contractAuthor, ...uis].filter(Boolean)`.\n","properties":{"contractAuthor":{"description":"Original publisher (`isContractAuthor: true`). Null only if the contract\nauthor row was deactivated but UI authors are still active.\n","oneOf":[{"$ref":"#/components/schemas/GameRecord"},{"type":"null"}]},"uis":{"type":"array","description":"Additional UI authors — publishers of the same `gamePuzzleHash` with a\ndifferent `authorPublicKey`. Excludes the `contractAuthor` entry.\n","items":{"$ref":"#/components/schemas/GameRecord"}}}},"PublishGameRequest":{"type":"object","required":["manifest","package"],"properties":{"manifest":{"$ref":"#/components/schemas/GameManifest"},"package":{"$ref":"#/components/schemas/GamePackage"}}},"DeleteGameRequest":{"type":"object","required":["signature"],"properties":{"signature":{"allOf":[{"$ref":"#/components/schemas/RequestSignature"}],"description":"Canonical message (action `delete`):\n```\ndelete\ngamePuzzleHash:<64-hex>\ngameType:<utf8-hex>\ngameUrl:<url>   OR  packageHash:<64-hex>\ntimestamp:<unix-seconds>\n```\n"}}},"RequestSignature":{"type":"object","required":["publicKey","message","sig","timestamp"],"properties":{"publicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"message":{"type":"string"},"sig":{"$ref":"#/components/schemas/BlsSignature"},"timestamp":{"type":"integer","description":"unix seconds"}}},"RoomStatus":{"type":"string","enum":["waiting","active","playing","closed"]},"StateChannelStatus":{"type":"string","enum":["pending","locked","active","funding","mempool","open","settling","settling_mempool","settled","cancelled"]},"RoomPlayer":{"type":"object","required":["name","walletAddress","walletPublicKey","identityPubKey","lastSeen"],"properties":{"name":{"type":"string","maxLength":50},"walletAddress":{"$ref":"#/components/schemas/ChiaAddress"},"walletPublicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"identityPubKey":{"$ref":"#/components/schemas/BlsPublicKey"},"lastSeen":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"unix millis of last activity."}}},"RoomPlayerInput":{"type":"object","required":["name","walletAddress","walletPublicKey","identityPubKey"],"properties":{"name":{"type":"string","maxLength":50},"walletAddress":{"$ref":"#/components/schemas/ChiaAddress"},"walletPublicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"identityPubKey":{"$ref":"#/components/schemas/BlsPublicKey"}}},"RoomRecord":{"type":"object","required":["roomId","status","gameType","publicRoom","wagerAmount","appBaseUrl","player1","player2","stateChannelCoinId","stateChannelStatus","data","createdAt","updatedAt"],"properties":{"roomId":{"type":"string"},"status":{"$ref":"#/components/schemas/RoomStatus"},"gameType":{"oneOf":[{"type":"string"},{"type":"null"}]},"publicRoom":{"type":"boolean"},"wagerAmount":{"type":"integer","minimum":0,"description":"Wager in mojos."},"appBaseUrl":{"type":"string","format":"uri"},"player1":{"$ref":"#/components/schemas/RoomPlayer"},"player2":{"oneOf":[{"$ref":"#/components/schemas/RoomPlayer"},{"type":"null"}]},"stateChannelCoinId":{"oneOf":[{"type":"string"},{"type":"null"}]},"stateChannelStatus":{"oneOf":[{"$ref":"#/components/schemas/StateChannelStatus"},{"type":"null"}]},"data":{"type":"object","additionalProperties":true},"createdAt":{"type":"integer"},"updatedAt":{"type":"integer"}}},"RoomListResult":{"type":"object","required":["total","offset","limit","rooms"],"properties":{"total":{"type":"integer"},"offset":{"type":"integer"},"limit":{"type":"integer"},"rooms":{"type":"array","items":{"$ref":"#/components/schemas/RoomRecord"}}}},"CreateRoomRequest":{"type":"object","required":["roomId","appBaseUrl","player1","signature"],"properties":{"roomId":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9_-]+$"},"appBaseUrl":{"type":"string","format":"uri"},"publicRoom":{"type":"boolean","default":true},"wagerAmount":{"type":"integer","minimum":0},"gameType":{"type":"string","pattern":"^[0-9a-f]+$","maxLength":256},"data":{"type":"object","additionalProperties":true},"player1":{"$ref":"#/components/schemas/RoomPlayerInput"},"signature":{"$ref":"#/components/schemas/RequestSignature"}}},"JoinRoomRequest":{"type":"object","required":["player2","signature"],"properties":{"player2":{"$ref":"#/components/schemas/RoomPlayerInput"},"signature":{"$ref":"#/components/schemas/RequestSignature"}}},"PatchRoomRequest":{"type":"object","required":["signature"],"properties":{"status":{"$ref":"#/components/schemas/RoomStatus"},"gameType":{"oneOf":[{"type":"string"},{"type":"null"}]},"wagerAmount":{"type":"integer","minimum":0},"stateChannelCoinId":{"oneOf":[{"type":"string","maxLength":66},{"type":"null"}]},"stateChannelStatus":{"oneOf":[{"$ref":"#/components/schemas/StateChannelStatus"},{"type":"null"}]},"data":{"type":"object","additionalProperties":true},"signature":{"$ref":"#/components/schemas/RequestSignature"}}},"CloseRoomRequest":{"type":"object","required":["signature"],"properties":{"signature":{"$ref":"#/components/schemas/RequestSignature"}}},"ReportResultRequest":{"type":"object","required":["gameId","gameType","wagerAmount","startedAt","endedAt","signature"],"properties":{"gameId":{"$ref":"#/components/schemas/Slug"},"gameType":{"type":"string","pattern":"^[0-9a-f]+$","maxLength":256},"wagerAmount":{"type":"integer","minimum":0},"winner":{"oneOf":[{"type":"string","enum":["player1","player2","draw"]},{"type":"null"}]},"player1Result":{"oneOf":[{"type":"integer"},{"type":"null"}]},"player2Result":{"oneOf":[{"type":"integer"},{"type":"null"}]},"startedAt":{"type":"integer","description":"unix millis"},"endedAt":{"type":"integer","description":"unix millis (>= startedAt)"},"signature":{"$ref":"#/components/schemas/RequestSignature"}}},"RoomGameRecord":{"type":"object","required":["id","roomId","gameId","gameType","wagerAmount","winner","player1WalletPublicKey","player2WalletPublicKey","player1Result","player2Result","startedAt","endedAt"],"properties":{"id":{"type":"integer"},"roomId":{"type":"string"},"gameId":{"type":"string"},"gameType":{"type":"string"},"wagerAmount":{"type":"integer","minimum":0},"winner":{"oneOf":[{"type":"string","enum":["player1","player2","draw"]},{"type":"null"}]},"player1WalletPublicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"player2WalletPublicKey":{"$ref":"#/components/schemas/BlsPublicKey"},"player1Result":{"oneOf":[{"type":"integer"},{"type":"null"}]},"player2Result":{"oneOf":[{"type":"integer"},{"type":"null"}]},"startedAt":{"type":"integer"},"endedAt":{"type":"integer"}}}}}}