# The Simplicity Guide — game API handoff This document is for the Codex implementing the guide inside the Simplicity game. The guide service is already implemented in this repository. Integrate it through HTTP; do not rebuild retrieval, copy wiki data into the game, or call OpenAI from the game client. ## Integration contract Player UI → authenticated game server → Simplicity Guide API → answer returned to that player. The API answers one independent question at a time using the public Simplicity wiki. It handles wiki freshness, source selection, the guide persona, model calls, token accounting, and caching. It does not require conversation IDs or player accounts. It cannot read the player's inventory, teleport them, grant rewards, or execute commands. Use these two environment variables on the **game server**: ```dotenv SIMPLICITY_GUIDE_BASE_URL=http://127.0.0.1:3000 SIMPLICITY_GUIDE_API_KEY=replace-with-the-service-CHAT_API_KEY ``` The URL above is for a guide service on the same machine. For separate servers, use the actual private service address or HTTPS URL. `localhost` on the game server does not refer to the developer's computer. No public deployment URL has been provisioned by this project. `SIMPLICITY_GUIDE_API_KEY` must equal this service's `CHAT_API_KEY`. It is **not** the OpenAI or Supabase key. Never embed this shared key in a downloadable game client or send it to a player. ## Endpoints | Method | Path | Authentication | Purpose | | --- | --- | --- | --- | | POST | `/api/v1/ask` | Bearer | Answer one question | | GET | `/api/v1/search?q=...` | Bearer | Search wiki passages without an AI call | | GET | `/health` | None | Configuration and index status; no model call | | GET | `/openapi.json` | None | OpenAPI 3.1 contract for clients/tools | | GET | `/docs` or `/docs/game-api.md` | None | This handoff document | | GET | `/docs/client.mjs` | None | Copyable server-side JavaScript client | The browser's existing `POST /api/chat` and `GET /api/search` remain compatibility aliases. Use `/api/v1/*` for new game integrations. The API returns JSON, not a token stream. Clients should tolerate additional response fields in v1. Authentication is required when `CHAT_API_KEY` is configured. It may be omitted only for local development with that setting blank. Non-loopback service binding requires a key of at least 24 characters. ## Ask a question ```http POST /api/v1/ask Authorization: Bearer Content-Type: application/json {"message":"I have 99 Slayer and Onyx rank. Can I do Elite Slayer duo tasks in my private instance?"} ``` `message` is required: a nonblank string, maximum 2,000 JavaScript UTF-16 code units before trimming. Total request body limit is **32,000 bytes**. Send only `message`. Old `history` and other unknown fields are ignored; they do not provide context. For a question that depends on the player's rank, gear, or progress, include a short factual description in this message. Add only relevant details that the game server knows. Avoid player names, unique IDs, timestamps, or the entire inventory: these are unnecessary and reduce answer-cache reuse. Keep formatting stable. Do not send passwords, login tokens, or private account information. A shell example (the variables must be set/exported in your shell; `.env` is not automatically loaded by curl): ```sh curl --max-time 75 --fail-with-body \ "${SIMPLICITY_GUIDE_BASE_URL%/}/api/v1/ask" \ -H "Authorization: Bearer $SIMPLICITY_GUIDE_API_KEY" \ -H 'Content-Type: application/json' \ --data '{"message":"How do I start Elite Slayer?"}' ``` Illustrative response; game facts, IDs, timestamps, and counts vary: ```json { "requestId": "c69ba44f-2b02-41db-9ea6-95ea8d81d302", "answer": "Your Onyx rank makes the instance free, but duo tasks need a public area. Private instances don't count toward duo progress.", "grounded": true, "sources": [ { "id": "S1", "title": "Elite Slayer Guide", "url": "/guide/elite-slayer", "excerpt": "Exact selected wiki text...", "source": "supabase-guide", "updatedAt": null } ], "indexedAt": "2026-09-24T08:00:00.000Z", "warnings": [], "context": { "mode": "selected-wiki", "includedDocuments": 1, "totalDocuments": 437, "estimatedTokens": 1800, "tokenBudget": 12000, "partialDocuments": 1, "compactedDocuments": 1, "savedEvidenceTokens": 3000 }, "usage": { "modelRequests": 1, "estimatedInputTokens": 2600, "inputTokens": 2600, "outputTokens": 91, "cachedInputTokens": 1200, "cacheWriteTokens": 0 } } ``` | Field | How to use it | | --- | --- | | `answer` | Display as the guide's plain-text reply, preserving newlines. | | `grounded` | True means the answer passed source-ID validation. It is not a guarantee that every claim is correct. False can be a useful clarification, uncertainty reply, or greeting: still display `answer`. | | `sources` | Source cards or an optional “Sources” action. IDs are request-local, not permanent wiki IDs. Excerpts can be long; do not put them all in the game chat. | | `sources[].url` | May be absent, absolute HTTP(S), or wiki-relative. Resolve relative paths against the real **wiki** origin, never the guide API or game origin. Allowlist your wiki host before opening external links. | | `warnings` | Freshness/coverage messages for a details view or logs. If cached data is stale, show a concise “Wiki data may be out of date” notice. They are not instructions to change settings from inside the game. | | `context` | Optional evidence coverage. Partial documents are selected sections, not proof that missing mechanics do not exist. | | `indexedAt`, `knowledgeFetchedAt` | Index/live-fetch timestamps when available; not guarantees that every page was edited at that time. | | `usage` | Diagnostics. Provider counts may be absent; absence means unknown, not zero. Input includes wiki text and instructions as well as the question. | | `requestId` | Per-HTTP-request ID for diagnostics; also returned as `X-Request-ID`. It is not a conversation ID or idempotency key. | `usage.cachedInputTokens` and `usage.cacheWriteTokens` are subsets of `inputTokens`, not additional tokens. A local answer-cache hit has `cachedAnswer: true`, `modelRequests: 0`, and zero new input/output tokens. Coalesced identical requests have `sharedRequest: true` and zero additional model tokens. Do not sum cache reads/writes onto input totals or assume a missing count is zero. ## Game behavior to implement 1. Add an “Ask the Simplicity Guide” interface/NPC option/command using the game's existing conventions. Keep the integration behind the authenticated game server. 2. Validate the question and apply a per-player cooldown. Allow at most one in-flight guide request per player. Begin with a modest cooldown (for example, five seconds) and tune it for your traffic. 3. Send the HTTP request asynchronously. **Never block the game tick thread.** Use the game's existing HTTP/JSON libraries; Java servers can use their existing async HTTP client or `HttpClient.sendAsync` on compatible JDKs. 4. Show a waiting indicator. On completion, schedule the UI update on the appropriate game thread and verify the player is still connected and awaiting that request. Discard stale responses after a new request or logout. 5. Show the speaker as **The Simplicity Guide** and display `answer` verbatim as text. Escape game markup/control codes using the game's existing sanitizer, wrap long lines, and support multiple pages if needed. Do not run another model to rewrite it or replace a `grounded: false` answer with a generic error. 6. Show optional source links and a compact stale-data notice where appropriate. Keep token usage and detailed operational errors in admin diagnostics unless the product explicitly needs them for players. 7. Suggested failure text: “I'm having trouble reaching the wiki right now. Give it a moment and try again.” Preserve the HTTP status, `code`, and `requestId` in server diagnostics, without logging bearer keys or whole player questions. The model's response is advice only. Never parse an answer as permission to execute a teleport, award items, change player state, or bypass game rules. ## Errors, timeouts, and limits Errors use the same JSON shape on both API versions: ```json { "error": "Too many requests. Please wait a minute.", "code": "RATE_LIMITED", "requestId": "c69ba44f-2b02-41db-9ea6-95ea8d81d302" } ``` | HTTP | `code` | Caller action | | --- | --- | --- | | 400 | `INVALID_REQUEST` | Fix the question/body; do not retry automatically. | | 401 | `UNAUTHORIZED` | Fix the server bearer key. Never ask the player for this key. | | 403 | `ORIGIN_NOT_ALLOWED` | Fix browser/proxy origin configuration. Server-to-server requests normally omit `Origin`. | | 404 | `NOT_FOUND` | Check the path and HTTP method. | | 413 | `REQUEST_TOO_LARGE` | Shorten the question/context; applies to body size or selected model context budget. | | 415 | `UNSUPPORTED_MEDIA_TYPE` | Send `Content-Type: application/json`. | | 429 | `RATE_LIMITED` | Honor `Retry-After` (integer seconds); re-enable asking after that delay. | | 502 | `AI_UNAVAILABLE` | Model/network/response failure. Offer a manual retry; a failed call may already have consumed tokens. | | 503 | `BUSY` | Service concurrency limit reached. `Retry-After: 2` is provided. | | 503 | `AI_NOT_CONFIGURED` | Operator must configure the service's OpenAI key. | | 500 | `INTERNAL_ERROR` | Record the request ID and notify the service operator. | Default quotas are **20 authenticated requests/minute per socket IP** and **4 concurrent answer requests per process**. All players behind one game server or reverse proxy share its IP quota. Set `RATE_LIMIT_PER_MINUTE` to an appropriate aggregate game-server budget and enforce per-player limits in the game. Forwarded IP headers are intentionally ignored. Quotas and answer caches are per process, not shared across replicas, and reset on service restart. Use a client timeout of about **75 seconds** and a reverse-proxy response timeout at least as long. The AI call itself times out after 45 seconds; wiki refresh can take additional time. The reference client never retries automatically. Do not blindly retry a timeout or 502: the server may still finish, and retries can consume additional tokens. A disconnected client does not guarantee cancellation of the server's model request. There is no durable exactly-once/idempotency contract. ## Server-side JavaScript reference Copy `examples/game-api-client.mjs` into your game backend, or fetch `/docs/client.mjs`. It needs Node.js 20.19+ and no dependencies. Other languages should follow the same wire contract or generate a client from OpenAPI. ```js import { SimplicityGuideClient, GuideApiError } from './game-api-client.mjs'; const guide = new SimplicityGuideClient({ baseUrl: process.env.SIMPLICITY_GUIDE_BASE_URL, apiKey: process.env.SIMPLICITY_GUIDE_API_KEY }); // Call inside the game's authenticated, rate-limited request handler. try { const result = await guide.ask('I have 99 Slayer. How do I start Elite Slayer?'); // Adapt to your game's UI and thread model: // replyToPlayer('The Simplicity Guide', escapeGameText(result.answer)); console.log({ requestId: result.requestId, usage: result.usage }); } catch (error) { if (error instanceof GuideApiError) { console.error({ status: error.status, code: error.code, requestId: error.requestId, retryAfterSeconds: error.retryAfterSeconds }); } else throw error; } ``` The client additionally exposes local `TIMEOUT`, `CANCELLED`, `NETWORK_ERROR`, and `INVALID_RESPONSE` codes. It accepts an `AbortSignal` in `ask(message, { signal })` and never automatically retries. `search(query)` returns `{ results, indexedAt, warnings, requestId, ... }`. `health()` does not send the bearer key. ## Vercel deployment For Vercel use the checked-in `vercel.json` and follow `docs/VERCEL.md`. Supply the keys and live wiki source in Vercel Environment Variables, then redeploy. Startup failures return `503 SERVICE_NOT_READY` with `Retry-After: 10`. The local sibling wiki directory is not deployed; index and answer-cache data live only in a warm function instance. Cold starts can clear cached answers before the six-hour expiry, and quotas are not global across instances. ## Running the guide service On the service host: ```sh npm ci # Create .env from .env.example only if you do not already have one. # Configure the OpenAI key and wiki/Supabase source there. npm start ``` For same-host game integration, keep `HOST=127.0.0.1`. Configure `CHAT_API_KEY` even on loopback when multiple applications share the machine. For cross-host use, set `HOST=0.0.0.0`, a random `CHAT_API_KEY` of at least 24 characters, and a private network or HTTPS reverse proxy. Do not expose the shared key over public HTTP. Keep service credentials in server environment/secret storage. Relevant service settings: ```dotenv HOST=127.0.0.1 PORT=3000 CHAT_API_KEY=replace-with-a-long-random-service-key OPENAI_API_KEY=your-server-openai-key OPENAI_MODEL=gpt-6-luna WIKI_BASE_URL=https://your-actual-wiki-host.example RATE_LIMIT_PER_MINUTE=20 MAX_CONCURRENT_REQUESTS=4 ANSWER_CACHE_TTL_SECONDS=21600 ANSWER_CACHE_MAX_ENTRIES=256 ``` Keep the existing wiki source configuration (`WIKI_SUPABASE_URL` plus public `WIKI_SUPABASE_ANON_KEY`, or `WIKI_API_URL`/`WIKI_PATH`). See the repository README for source coverage and indexing. Configure `ALLOWED_ORIGINS` with exact browser origins only if a trusted browser/admin UI will call this service, including the public service origin behind a proxy. CORS does not authenticate players. `GET /health` returns `status`, `aiConfigured`, `authRequired`, index `passages`, `source`, timestamps/coverage when available, warnings, and a request ID. HTTP 200 only proves the process responds. `aiConfigured: true` means a key is present; it does not test account credit, model access, or wiki freshness. It makes no paid request. Protect production detailed health/docs at your gateway if they should not be public. Run under your existing process supervisor/container platform. Keep the generated index in persistent storage for fallback; with Supabase configured startup rebuilds it and needs network access. The browser UI remains available as an operator test interface. This handoff does not deploy a public server, change your real keys, or alter the game's repository. ## Acceptance checks for the integrating Codex - A real authenticated player can submit a question and see a correctly wrapped guide reply. - Calls use `/api/v1/ask` from the game backend, with secrets absent from client assets and packets. - Missing/invalid service credentials produce a handled failure; the UI stops waiting. - Clarifying answers with `grounded: false` still display. - Newline handling, long answers, source links, logout, cancellation, and out-of-order responses work. - Per-player cooldown and one-request-at-a-time enforcement protect the shared service quota. - 429/BUSY wait periods are respected; 502/timeouts are not automatically retried. - Use mocked HTTP responses for routine integration tests. Make a live game-question smoke test only when the operator intends to spend API tokens. Service verification: `npm run check` (mocked API, temporary HTTP servers) and `npm run eval:context` (offline saved-index regression checks). Real answers still need human review; valid citations do not prove every game fact is correct. ## Paste this to the other Codex > Integrate The Simplicity Guide into our game using the attached GAME_API_HANDOFF.md and OpenAPI contract. Inspect our game code and use its existing async HTTP, JSON, threading, UI, and configuration conventions. The game server must call POST /api/v1/ask with {"message":"..."}; load SIMPLICITY_GUIDE_BASE_URL and SIMPLICITY_GUIDE_API_KEY from server configuration. Never put the shared API key in the game client. Add a player-facing Ask the Simplicity Guide entry point, one in-flight request per player, a cooldown, loading/error states, and safely rendered plain-text answers. Keep questions stateless, include only relevant server-known player context, and show useful clarification replies even when grounded is false. Preserve optional sources and diagnostics, respect Retry-After, and avoid automatic retries after ambiguous failures. Use mocks for tests and report the configuration and any remaining deployment prerequisites. Do not modify game state based on AI output or rebuild the guide's wiki/model logic in the game.