# GemTavern Game Link API Full Context ## Protocol guide # GemTavern Game Link Protocol v1 Game Link lets a local game mod send a character card, current scene, and prompt directives to GemTavern over a local WebSocket. The game mod owns game-specific interpretation: - Which character is speaking. - What scene is happening. - Whether a live event should trigger a reply. - The hidden prompt directive for automatic events and user messages. GemTavern owns generic runtime behavior: - Pairing and local WebSocket endpoints. - Character import and chat binding. - Prompt assembly from `promptDirective`. - Model calls, voice, UI, storage, and TTS. ## Transport First version uses local WebSocket: ```text ws://{device-host}:38471/game-link/socket ``` The mod sends one JSON object per WebSocket message. ## Payload Envelope Every v1 payload uses: ```json { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "character_card_upsert", "integration": { "id": "rimworld", "gameId": "rimworld", "gameName": "RimWorld", "modName": "GemTavern Game Link", "modVersion": "1.0.0" }, "character": { "id": "stable-game-character-id", "name": "Max", "cardJSON": "{...}", "avatarPNGBase64": "..." }, "scene": { "kind": "wedding", "family": "wedding", "priority": 50, "line": "Max is marrying Ballard." }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\nMax is marrying Ballard.", "autoEventGuide": "[Game event reply guide]\n...", "directUserGuide": "[Game direct user message]\n...", "promptVersion": "rimworld-prompts-2026-07-02" }, "event": { "id": "optional-stable-event-id", "visibleText": "Max is marrying Ballard.", "replyPolicy": "auto", "createdAtUnix": 1780000000 }, "state": {} } ``` ## Types ### `character_card_upsert` Creates or updates a Game Link character in GemTavern. Required: - `integration` - `character.id` - `character.name` - `character.cardJSON` - `promptDirective` Recommended: - `character.avatarPNGBase64` - `scene` - `state` ### `live_event` Sends a live game moment. Required: - `integration` - `character.id` - `character.name` - `event.visibleText` - `event.replyPolicy` - `promptDirective` If `event.replyPolicy == "auto"`, GemTavern may generate a character reply. If `event.replyPolicy == "silent"`, GemTavern updates state without asking the model for an automatic reply. ## Prompt Rules GemTavern assembles prompts from mod-provided directives: - Automatic event: `promptDirective.sceneContext + promptDirective.autoEventGuide` - User text or voice: `promptDirective.directUserGuide + promptDirective.sceneContext` GemTavern may append app-owned runtime settings, such as how the character should address the user. GemTavern does not infer game-specific scene routing from `state`. ## Field Rules - `integration.id`: stable lowercase integration identifier, such as `rimworld`. - `integration.gameId`: stable game identifier. - `character.id`: stable ID from the game save, not a display name. - `scene.line`: natural user-facing scene sentence. - `state`: opaque game-specific JSON. GemTavern stores it but does not interpret it for scene routing. - `promptDirective`: required for product flows. Missing directives should be treated as integration errors. ## QR Pairing QR payload type: ```text gemtavern_game_link_pair_v1 ``` Required QR fields: - `protocol`: `gemtavern.game_link` - `protocolVersion`: `1` - `type`: `gemtavern_game_link_pair_v1` - `integration` - `token` - `hosts` - `responsePort` - `expiresAtUnix` GemTavern responds with candidate WebSocket endpoints. Mods should try all candidates and save the first successful endpoint. ## Mod author guide # Game Link Mod Author Guide This guide is for game mod authors who want their game characters to chat through GemTavern. ## Responsibilities Your mod should provide: - A stable character ID and display name. - A GemTavern character card JSON. - A natural scene line. - Prompt directives for automatic game events and user messages. - Live events with `replyPolicy`. GemTavern provides: - Local pairing and WebSocket listener. - Character import and chat binding. - Model and voice runtime. - UI and chat history. ## Minimal Flow 1. Show a Game Link QR code in your game mod. 2. GemTavern scans it and returns WebSocket endpoint candidates. 3. Your mod sends `character_card_upsert`. 4. When something worth reacting to happens, your mod sends `live_event`. ## Character Card Upsert ```json { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "character_card_upsert", "integration": { "id": "example_game", "gameId": "example_game", "gameName": "Example Game", "modName": "Example Game Link", "modVersion": "0.1.0" }, "character": { "id": "save-123:npc-45", "name": "Mira", "cardJSON": "{\"spec\":\"chara_card_v2\",\"data\":{\"name\":\"Mira\",\"description\":\"...\"}}" }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\nMira is standing near the old gate.", "autoEventGuide": "[Game event reply guide]\nReply as Mira about this moment.", "directUserGuide": "[Game direct user message]\nReply as Mira to the user's message first.", "promptVersion": "example-prompts-0.1.0" }, "state": {} } ``` ## Live Event ```json { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "live_event", "integration": { "id": "example_game", "gameId": "example_game", "gameName": "Example Game", "modName": "Example Game Link", "modVersion": "0.1.0" }, "character": { "id": "save-123:npc-45", "name": "Mira" }, "scene": { "kind": "ambush", "family": "danger", "priority": 80, "line": "Mira hears footsteps behind the old gate." }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\nMira hears footsteps behind the old gate.", "autoEventGuide": "[Game event reply guide]\nSpeak as Mira reacting to this danger.", "directUserGuide": "[Game direct user message]\nReply to the user first, using the scene as context.", "promptVersion": "example-prompts-0.1.0" }, "event": { "id": "save-123:event-999", "visibleText": "Mira hears footsteps behind the old gate.", "replyPolicy": "auto", "createdAtUnix": 1780000000 }, "state": {} } ``` ## Prompt Directive Guidance Keep directives game-specific and concrete. GemTavern will not guess your game state. Recommended: - Put the current scene in `sceneContext`. - Put automatic-event behavior in `autoEventGuide`. - Put user-message behavior in `directUserGuide`. - Version your prompt text with `promptVersion`. Avoid: - Raw internal field names in `scene.line`. - Huge state dumps in model-facing prompt text. - Requiring GemTavern to infer game-specific rules from opaque `state`. ## Debugging Validate a payload: ```bash ruby tools/validate-game-link-payload.rb docs/game-link-examples/fake-live-event.json ``` Send with websocat: ```bash websocat ws://127.0.0.1:38471/game-link/socket < docs/game-link-examples/fake-live-event.json ``` ## Overview # Game Link Overview Game Link is a local integration protocol for game mods that want their in-game characters to chat through GemTavern. The mod owns game-specific meaning: which character is speaking, what scene is happening, which moments deserve a reply, and what hidden prompt guidance should be used. GemTavern owns the generic runtime: pairing, the local WebSocket listener, character import, chat binding, model calls, voice, storage, and TTS. ## Core Flow 1. The game mod shows a Game Link QR code or otherwise obtains a local WebSocket endpoint. 2. GemTavern scans the QR code and sends endpoint candidates back to the mod. 3. The mod sends `character_card_upsert` with a stable game character ID and a SillyTavern-compatible character card. 4. The mod sends `live_event` whenever a game moment should update chat context or trigger an automatic reply. 5. GemTavern binds the game character to a chat and combines the latest `promptDirective` with app-owned runtime settings. ## Transport The v1 runtime uses one JSON object per WebSocket message: ```text ws://{device-host}:38471/game-link/socket ``` For local simulator or same-machine testing, start with: ```text ws://127.0.0.1:38471/game-link/socket ``` For a phone or iPad on the same LAN, use the LAN address shown by GemTavern, such as: ```text ws://192.168.1.42:38471/game-link/socket ``` ## Message Types `character_card_upsert` creates or updates a Game Link character in GemTavern. Send it before live events, after pairing, and whenever the game character card changes. `live_event` sends a moment from the running game. Use `event.replyPolicy: "auto"` when GemTavern may generate a reply and `"silent"` when the event should only refresh state. ## Prompt Directive Contract `promptDirective` is required for product integrations. GemTavern does not infer game-specific scene routing from opaque `state`. Use: ```text Automatic event prompt = promptDirective.sceneContext + promptDirective.autoEventGuide User text or voice prompt = promptDirective.directUserGuide + promptDirective.sceneContext ``` Keep `state` useful for storage and diagnostics, but put model-facing instructions in `promptDirective`. ## Pairing # QR Pairing QR pairing gives a mod a short-lived way to discover the GemTavern WebSocket endpoint without requiring the player to copy an IP address. ## QR Payload The mod renders a QR code containing a JSON payload: ```json { "type": "gemtavern_game_link_pair_v1", "protocol": "gemtavern.game_link", "protocolVersion": 1, "integration": { "id": "rimworld", "gameId": "rimworld", "gameName": "RimWorld", "modName": "GemTavern Game Link", "modVersion": "1.0.0" }, "token": "single-use-random-token", "hosts": ["192.168.1.20", "127.0.0.1"], "port": 38473, "responsePort": 38473, "expiresAtUnix": 1780000000 } ``` `token` must be random and single-use. `expiresAtUnix` should be short; the RimWorld test mod uses 120 seconds. `hosts` should contain local IPv4 candidates for the machine running the game. ## Response Transport After scanning, GemTavern sends a UDP response to the scanned host candidates at `responsePort` (`38473` in the RimWorld implementation). The response is newline-delimited text: ```text MINITAVERN_GAMELINK_PAIR_RESPONSE_V1 token=single-use-random-token endpoint=ws://192.168.1.42:38471/game-link/socket endpoint=ws://127.0.0.1:38471/game-link/socket ``` The mod should ignore responses with a missing header, token mismatch, expired QR payload, or no usable `ws://` or `wss://` endpoint. ## Endpoint Selection GemTavern may send multiple endpoint candidates. The mod should try them in order and persist the first endpoint that accepts `character_card_upsert`. The RimWorld mod stores the successful endpoint and disables auto-discovery after a QR pairing succeeds. ## Debugging # Debugging And Validation ## Validate Payloads The source app includes a Ruby validator for Game Link payloads: ```bash ruby Tools/validate-game-link-payload.rb docs/game-link-examples/fake-live-event.json ``` The validator checks the public contract: `protocol`, `protocolVersion`, `type`, required integration fields, character identity, prompt directives, card payloads, and live-event `replyPolicy`. ## Send A Test Event With GemTavern running locally: ```bash websocat ws://127.0.0.1:38471/game-link/socket < docs/game-link-examples/fake-live-event.json ``` If GemTavern is running on another device, replace `127.0.0.1` with the device host shown in the app. ## Pairing Checklist Check these in order: 1. The game and GemTavern device are on the same local network. 2. The QR payload has `type: "gemtavern_game_link_pair_v1"` and `protocolVersion: 1`. 3. The QR payload has not expired. 4. The mod is listening for UDP responses on `responsePort`. 5. The UDP response begins with `MINITAVERN_GAMELINK_PAIR_RESPONSE_V1`. 6. The response `token` matches the QR payload token. 7. At least one endpoint begins with `ws://` or `wss://`. 8. The mod tries all endpoint candidates before reporting failure. ## Common Integration Errors Missing `promptDirective` usually means the mod is asking GemTavern to infer game-specific behavior from `state`. Move model-facing context into `sceneContext`, `autoEventGuide`, and `directUserGuide`. Unstable `character.id` causes duplicate chats or broken bindings. Use a save-stable game object ID. Overlarge `state` makes debugging harder and can slow local transport. Keep model-facing prompt text concise and put only useful diagnostic data in `state`. Raw game field names in `scene.line` make chat feel mechanical. Use natural user-facing sentences like `Mira hears footsteps behind the old gate.` ## JSON Schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://gemtavern.app/schemas/game-link-protocol-v1.schema.json", "title": "GemTavern Game Link Protocol v1", "type": "object", "additionalProperties": true, "required": ["protocol", "protocolVersion", "type", "integration", "character", "promptDirective"], "properties": { "protocol": { "const": "gemtavern.game_link" }, "protocolVersion": { "const": 1 }, "type": { "type": "string", "enum": ["character_card_upsert", "live_event"] }, "integration": { "type": "object", "additionalProperties": true, "required": ["id", "gameId", "gameName"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 80 }, "gameId": { "type": "string", "minLength": 1, "maxLength": 80 }, "gameName": { "type": "string", "minLength": 1, "maxLength": 120 }, "modName": { "type": "string", "maxLength": 120 }, "modVersion": { "type": "string", "maxLength": 80 } } }, "character": { "type": "object", "additionalProperties": true, "required": ["id", "name"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 160 }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "cardJSON": { "type": "string", "maxLength": 500000 }, "avatarPNGBase64": { "type": "string", "maxLength": 3000000 } } }, "scene": { "type": "object", "additionalProperties": true, "properties": { "kind": { "type": "string", "maxLength": 80 }, "family": { "type": "string", "maxLength": 80 }, "priority": { "type": "number" }, "line": { "type": "string", "maxLength": 1000 } } }, "promptDirective": { "type": "object", "additionalProperties": true, "required": ["protocolVersion", "sceneContext", "autoEventGuide", "directUserGuide"], "properties": { "protocolVersion": { "type": "integer", "minimum": 1 }, "sceneContext": { "type": "string", "minLength": 1, "maxLength": 12000 }, "autoEventGuide": { "type": "string", "minLength": 1, "maxLength": 16000 }, "directUserGuide": { "type": "string", "minLength": 1, "maxLength": 16000 }, "promptVersion": { "type": "string", "maxLength": 120 } } }, "event": { "type": "object", "additionalProperties": true, "properties": { "id": { "type": "string", "maxLength": 160 }, "visibleText": { "type": "string", "maxLength": 2000 }, "replyPolicy": { "type": "string", "enum": ["auto", "silent"] }, "createdAtUnix": { "type": "number" } } }, "state": { "type": "object", "additionalProperties": true } }, "allOf": [ { "if": { "properties": { "type": { "const": "character_card_upsert" } } }, "then": { "properties": { "character": { "required": ["id", "name", "cardJSON"] } } } }, { "if": { "properties": { "type": { "const": "live_event" } } }, "then": { "required": ["event"], "properties": { "event": { "required": ["visibleText", "replyPolicy"] } } } } ] } ``` ## Character card example ```json { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "character_card_upsert", "integration": { "id": "example_game", "gameId": "example_game", "gameName": "Example Game", "modName": "Example Game Link", "modVersion": "0.1.0" }, "character": { "id": "save-123:npc-45", "name": "Mira", "cardJSON": "{\"spec\":\"chara_card_v2\",\"spec_version\":\"2.0\",\"data\":{\"name\":\"Mira\",\"description\":\"Mira is a watchful gate guard from Example Game.\"}}" }, "scene": { "kind": "watching_gate", "family": "generic", "priority": 10, "line": "Mira is standing near the old gate." }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\nMira is standing near the old gate.", "autoEventGuide": "[Game event reply guide]\nReply as Mira about this moment.", "directUserGuide": "[Game direct user message]\nReply as Mira to the user's message first.", "promptVersion": "example-prompts-0.1.0" }, "state": {} } ``` ## Live event example ```json { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "live_event", "integration": { "id": "example_game", "gameId": "example_game", "gameName": "Example Game", "modName": "Example Game Link", "modVersion": "0.1.0" }, "character": { "id": "save-123:npc-45", "name": "Mira" }, "scene": { "kind": "ambush", "family": "danger", "priority": 80, "line": "Mira hears footsteps behind the old gate." }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\nMira hears footsteps behind the old gate.", "autoEventGuide": "[Game event reply guide]\nSpeak as Mira reacting to this danger.", "directUserGuide": "[Game direct user message]\nReply to the user first, using the scene as context.", "promptVersion": "example-prompts-0.1.0" }, "event": { "id": "save-123:event-999", "visibleText": "Mira hears footsteps behind the old gate.", "replyPolicy": "auto", "createdAtUnix": 1780000000 }, "state": {} } ``` ## AsyncAPI ```json { "asyncapi": "3.0.0", "id": "urn:app:gemtavern:game-link:v1", "info": { "title": "GemTavern Game Link Protocol", "version": "1.0.0", "description": "Local WebSocket protocol used by game mods to send character cards, live scene events, and prompt directives to GemTavern." }, "defaultContentType": "application/json", "servers": { "local": { "host": "{deviceHost}:38471", "pathname": "/game-link/socket", "protocol": "ws", "description": "Local GemTavern listener on the user's device.", "variables": { "deviceHost": { "default": "127.0.0.1", "description": "GemTavern device hostname or LAN IP address." } } } }, "channels": { "gameLinkSocket": { "address": "/game-link/socket", "title": "Game Link WebSocket", "summary": "Game mods send one JSON object per WebSocket message.", "servers": [ { "$ref": "#/servers/local" } ], "messages": { "characterCardUpsert": { "$ref": "#/components/messages/CharacterCardUpsert" }, "liveEvent": { "$ref": "#/components/messages/LiveEvent" } }, "bindings": { "ws": { "method": "GET" } } } }, "operations": { "sendCharacterCardUpsert": { "action": "send", "channel": { "$ref": "#/channels/gameLinkSocket" }, "title": "Send character card", "summary": "Create or update a Game Link character in GemTavern.", "messages": [ { "$ref": "#/channels/gameLinkSocket/messages/characterCardUpsert" } ] }, "sendLiveEvent": { "action": "send", "channel": { "$ref": "#/channels/gameLinkSocket" }, "title": "Send live event", "summary": "Send a live game moment that may update state or trigger a character reply.", "messages": [ { "$ref": "#/channels/gameLinkSocket/messages/liveEvent" } ] } }, "components": { "messages": { "CharacterCardUpsert": { "name": "character_card_upsert", "title": "Character card upsert", "summary": "Creates or updates a Game Link character in GemTavern.", "payload": { "$ref": "#/components/schemas/CharacterCardUpsertPayload" }, "examples": [ { "name": "Fake character card upsert", "payload": { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "character_card_upsert", "integration": { "id": "example_game", "gameId": "example_game", "gameName": "Example Game", "modName": "Example Game Link", "modVersion": "0.1.0" }, "character": { "id": "save-123:npc-45", "name": "Mira", "cardJSON": "{\"spec\":\"chara_card_v2\",\"data\":{\"name\":\"Mira\",\"description\":\"...\"}}" }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\\nMira is standing near the old gate.", "autoEventGuide": "[Game event reply guide]\\nReply as Mira about this moment.", "directUserGuide": "[Game direct user message]\\nReply as Mira to the user's message first.", "promptVersion": "example-prompts-0.1.0" }, "state": {} } } ] }, "LiveEvent": { "name": "live_event", "title": "Live event", "summary": "Sends a live game moment to GemTavern.", "payload": { "$ref": "#/components/schemas/LiveEventPayload" }, "examples": [ { "name": "Fake live event", "payload": { "protocol": "gemtavern.game_link", "protocolVersion": 1, "type": "live_event", "integration": { "id": "example_game", "gameId": "example_game", "gameName": "Example Game", "modName": "Example Game Link", "modVersion": "0.1.0" }, "character": { "id": "save-123:npc-45", "name": "Mira" }, "scene": { "kind": "ambush", "family": "danger", "priority": 80, "line": "Mira hears footsteps behind the old gate." }, "promptDirective": { "protocolVersion": 1, "sceneContext": "[Game scene context]\\nMira hears footsteps behind the old gate.", "autoEventGuide": "[Game event reply guide]\\nSpeak as Mira reacting to this danger.", "directUserGuide": "[Game direct user message]\\nReply to the user first, using the scene as context.", "promptVersion": "example-prompts-0.1.0" }, "event": { "id": "save-123:event-999", "visibleText": "Mira hears footsteps behind the old gate.", "replyPolicy": "auto", "createdAtUnix": 1780000000 }, "state": {} } } ] } }, "schemas": { "BaseEnvelope": { "type": "object", "additionalProperties": true, "required": ["protocol", "protocolVersion", "type", "integration", "character", "promptDirective"], "properties": { "protocol": { "const": "gemtavern.game_link" }, "protocolVersion": { "const": 1 }, "type": { "type": "string", "enum": ["character_card_upsert", "live_event"] }, "integration": { "$ref": "#/components/schemas/Integration" }, "character": { "$ref": "#/components/schemas/Character" }, "scene": { "$ref": "#/components/schemas/Scene" }, "promptDirective": { "$ref": "#/components/schemas/PromptDirective" }, "event": { "$ref": "#/components/schemas/Event" }, "state": { "type": "object", "additionalProperties": true } } }, "CharacterCardUpsertPayload": { "allOf": [ { "$ref": "#/components/schemas/BaseEnvelope" }, { "type": "object", "required": ["character"], "properties": { "type": { "const": "character_card_upsert" }, "character": { "allOf": [ { "$ref": "#/components/schemas/Character" }, { "type": "object", "required": ["id", "name", "cardJSON"] } ] } } } ] }, "LiveEventPayload": { "allOf": [ { "$ref": "#/components/schemas/BaseEnvelope" }, { "type": "object", "required": ["event"], "properties": { "type": { "const": "live_event" }, "event": { "allOf": [ { "$ref": "#/components/schemas/Event" }, { "type": "object", "required": ["visibleText", "replyPolicy"] } ] } } } ] }, "Integration": { "type": "object", "additionalProperties": true, "required": ["id", "gameId", "gameName"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 80, "description": "Stable lowercase integration identifier, such as rimworld." }, "gameId": { "type": "string", "minLength": 1, "maxLength": 80 }, "gameName": { "type": "string", "minLength": 1, "maxLength": 120 }, "modName": { "type": "string", "maxLength": 120 }, "modVersion": { "type": "string", "maxLength": 80 } } }, "Character": { "type": "object", "additionalProperties": true, "required": ["id", "name"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 160, "description": "Stable ID from the game save, not a display name." }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "cardJSON": { "type": "string", "maxLength": 500000, "description": "SillyTavern-compatible character card JSON as a string." }, "avatarPNGBase64": { "type": "string", "maxLength": 3000000 } } }, "Scene": { "type": "object", "additionalProperties": true, "properties": { "kind": { "type": "string", "maxLength": 80 }, "family": { "type": "string", "maxLength": 80 }, "priority": { "type": "number" }, "line": { "type": "string", "maxLength": 1000, "description": "Natural user-facing scene sentence." } } }, "PromptDirective": { "type": "object", "additionalProperties": true, "required": ["protocolVersion", "sceneContext", "autoEventGuide", "directUserGuide"], "properties": { "protocolVersion": { "type": "integer", "minimum": 1 }, "sceneContext": { "type": "string", "minLength": 1, "maxLength": 12000 }, "autoEventGuide": { "type": "string", "minLength": 1, "maxLength": 16000 }, "directUserGuide": { "type": "string", "minLength": 1, "maxLength": 16000 }, "promptVersion": { "type": "string", "maxLength": 120 } } }, "Event": { "type": "object", "additionalProperties": true, "properties": { "id": { "type": "string", "maxLength": 160 }, "visibleText": { "type": "string", "maxLength": 2000 }, "replyPolicy": { "type": "string", "enum": ["auto", "silent"] }, "createdAtUnix": { "type": "number" } } }, "PairingQrPayload": { "type": "object", "required": ["type", "protocol", "protocolVersion", "integration", "token", "hosts", "responsePort", "expiresAtUnix"], "properties": { "type": { "const": "gemtavern_game_link_pair_v1" }, "protocol": { "const": "gemtavern.game_link" }, "protocolVersion": { "const": 1 }, "integration": { "$ref": "#/components/schemas/Integration" }, "token": { "type": "string", "minLength": 1 }, "hosts": { "type": "array", "items": { "type": "string" } }, "port": { "type": "integer", "default": 38473 }, "responsePort": { "type": "integer", "default": 38473 }, "expiresAtUnix": { "type": "number" } } } } } } ```