AGENTS.md for your game
AGENTS.md is the rules file that AI coding agents (Claude Code, Cursor, Codex and similar) read automatically from the root of your repository. When agents connect a game to Open Play, the mistakes they make most often are: guessing an endpoint or header, putting the server key in the client, skipping the CSRF and session rules, not pinning the plugin schema, removing a domain too early, and leaving stale cached JavaScript running in production. The template on this page puts the rules that prevent those mistakes into one file. The rules come from problems met in real game integrations (the example games ArenaRace and LanternTide), and every rule links to the docs page behind it.
How to use it
- Download the template:
https://open-play.b4klabs.dev/docs/AGENTS.md(or copy the block below) and save it asAGENTS.mdin the root of your game repository. If you already have anAGENTS.md, add the template under an “Open Play integration” heading. - In “Project facts”, replace the placeholders
<YOUR_DEV_ENV_ID>,<YOUR_PROD_ENV_ID>,<OPEN_PLAY_BASE_URL>,<DEV_GAME_ORIGIN>,<PROD_GAME_ORIGIN>and<SDK_VERSION>with your values. Environment ids are not secret; do not write keys or secrets into the file. - Add the Open Play MCP server to your agent (Using AI tools):
claude mcp add --transport http open-play https://open-play.b4klabs.dev/mcp. The template tells the agent to usesearch_docs,read_doc,search_sdkandget_sdk_symbolfirst, so it takes behavior from the current docs and not from memory. - Delete the sections your game does not use (for example section 6 if you use no plugins). Add your own rules (code style, test commands, branch workflow) above or below the template.
What the template covers
| Section | What it prevents |
|---|---|
| MCP first | Endpoints, headers and field names written from guesses |
| Keys and environments | A server key leaking into the client, dev and prod mixed up |
| Sign-in and sessions | Guest flow, Google popup with a redirect fallback, the redirect base (/auth/callback), the CSRF race, single active session, account deletion |
| Domains and browser | Unregistered origins, removing a live domain too early, a proxy dropping headers |
| Player data | A separate save server, writing without If-Match |
| Plugins | Schema mismatch, a command resent with a new key, a disabled grant after a version bump |
| Realtime | Ticket reuse, replayed inputs, a one-at-a-time input flow that feels laggy |
| Caching and deploys | A CDN overriding no-cache and keeping old JavaScript |
| Errors and limits | Parsing error text, a shared per-IP bucket |
| Testing and Do / Don’t | A checklist before you say “done” |
The template is in English because AI tools and the MCP index of the docs site work best with English text. It is also listed in the “Optional” section of the https://open-play.b4klabs.dev/docs/llms.txt index.
Note
The template is a rules file, not a guarantee from Open Play. The docs pages are the only source of truth for behavior; if the template and a page disagree, the page is right. We recommend trying the template in a real agent session and tightening it for your game.
The full template
# AGENTS.md — integrating this game with Open Play
> Drop this file in the root of your game repository. Replace every `<PLACEHOLDER>` and delete rules that do not apply.
> Source and updates: https://open-play.b4klabs.dev/docs/AGENTS.md
This game uses **Open Play**, a game backend platform: player identity and sessions, player data (cloud save), characters, economy, store, rewards, social features, leaderboards, notifications, plugins and realtime connections. The game talks to Open Play over HTTP and WebSocket, through the TypeScript SDK (`@ferdiunal/open-play-ts`) or the Unity SDK. The game does **not** run its own account or save server for anything Open Play provides.
Docs root: https://open-play.b4klabs.dev/docs/en/ — every rule below links to its page.
## 0. Project facts (fill in)
- Environment ids (the `x-crx-env` value): dev `<YOUR_DEV_ENV_ID>`, prod `<YOUR_PROD_ENV_ID>`. One environment per stage; data is never shared between them.
- Open Play base URL: `<OPEN_PLAY_BASE_URL>` (the API address the browser calls).
- Game origins registered per environment: `<DEV_GAME_ORIGIN>`, `<PROD_GAME_ORIGIN>`.
- SDK version (exact, no `^`, no `latest`): `<SDK_VERSION>`.
## 1. Use the Open Play MCP server first
Before you write any call to Open Play, **look it up**. Never guess an endpoint, header, field or error code.
```bash
claude mcp add --transport http open-play https://open-play.b4klabs.dev/mcp
```
Cursor (`.cursor/mcp.json`) and VS Code (`.vscode/mcp.json`, key `servers`): `{"mcpServers":{"open-play":{"url":"https://open-play.b4klabs.dev/mcp"}}}`.
- `search_docs` then `read_doc` (for example `auth/oauth`) for behavior and HTTP contracts.
- `search_sdk` then `get_sdk_symbol` (for example `PlayerSession`, `sdk: "typescript"` or `"unity"`) for exact SDK signatures.
- The server is read-only and public; it never needs or sees a key. If the docs and your memory disagree, the docs win.
- No MCP available: read https://open-play.b4klabs.dev/docs/llms.txt (index) or `llms-full.txt` (everything).
Docs: [Using AI tools (MCP)](https://open-play.b4klabs.dev/docs/en/getting-started/ai-tools.html).
## 2. Keys and environments
- **Client** code (browser, WebGL, mobile) knows only the environment id and the player session. It carries **no secret**.
- A **server key** (`crx_live_<prefix>_<secret>`) lives only on your own server or backend-for-frontend (BFF): in its secret store, never in the client bundle, the repository, logs, or `.env` files that are committed. Use a separate key per environment and service, with an expiry.
- A client key is not a server key; calling a server endpoint with it is rejected. Player operations use the player session, not a key.
- Never point a dev build at the prod environment or the reverse. Read the environment id from per-stage configuration.
- Do not hard-code or print secrets, tokens, cookies or `x-csrf-token` values in code, tests, logs or analytics.
Docs: [Security](https://open-play.b4klabs.dev/docs/en/operations/security.html), [API keys](https://open-play.b4klabs.dev/docs/en/panel/game-settings-api-keys.html), [Production checklist](https://open-play.b4klabs.dev/docs/en/operations/production-checklist.html).
## 3. Sign-in and sessions
- **Guest first.** Open a guest session on first launch so the player plays at once; offer Google (or email) later and **link** it to the same player. Linking a guest upgrades the same player id; progress is kept.
- **Google OAuth:** try a popup window and fall back to a full-page redirect when the popup is blocked. Open the popup synchronously inside the click handler, before any `await`. Let the callback page hand the result back (for example through `BroadcastChannel`) and close itself.
- The server builds the Google `redirect_uri` from the configured **redirect base + `/google`**. The base must end at `/auth/callback` (not `/google`); the redirect URI registered with Google must be exactly `<base>/google`, and that path must be a page of your game that finishes the sign-in.
- The OAuth `code` and `state` are single-use: finish exactly once, then remove them from the address bar (`history.replaceState`). Use the SDK helpers; they bind `state` to the browser tab.
- A cookie session sends `x-crx-env` on every call and a CSRF token on every state-changing request. The token is **single-use and rotates**: never send two mutations at once with the same token, and keep the new token from each response. The SDKs serialize this for you; share the token between tabs with the SDK `csrf.channel` option. Retry a CSRF `403` once with a fresh token.
- Session lifetime is 30 days sliding, 180 days absolute. On `401` refresh or sign in again; do not loop.
- If the environment allows a **single active session**, a newer sign-in revokes the older one. Handle `reauthRequired` with a clear "signed in on another device" screen and a sign-in button, not a silent retry.
- Show the sign-in methods you actually enabled; hide the rest. Do not use SDK methods the docs mark as unsupported.
- **Account deletion** is a store requirement (Apple, Google Play): provide an in-game "Delete my account" action (`POST /v1/auth/account/delete`, `confirm: "delete_my_account"`). A linked account must have signed in within the last 10 minutes (`recent_sign_in_required`): ask the player to sign in again, then repeat.
- Verify identity on your own game server with the access JWT and the JWKS, never by trusting a client-sent player id.
Docs: [Auth overview](https://open-play.b4klabs.dev/docs/en/auth/overview.html), [Guest](https://open-play.b4klabs.dev/docs/en/auth/guest.html), [Google sign-in](https://open-play.b4klabs.dev/docs/en/auth/oauth.html), [Sessions and CSRF](https://open-play.b4klabs.dev/docs/en/auth/sessions-and-csrf.html), [Account linking](https://open-play.b4klabs.dev/docs/en/auth/account-linking.html), [Account deletion](https://open-play.b4klabs.dev/docs/en/auth/account-deletion.html), [Server-side verification](https://open-play.b4klabs.dev/docs/en/auth/server-verification.html).
## 4. Domains and calling from the browser
- Register **every origin your game runs on** in each environment's domain list (Game settings, Environments and domains): one full domain per line, no wildcards, no ports, at most 10, and a domain belongs to one environment only. An unregistered origin gets `403` on writes.
- Call Open Play either directly (your game domain is in the list; the server sends CORS headers with credentials) or through a same-origin reverse proxy that forwards `Cookie`, `Origin`, `x-crx-env`, `x-csrf-token`, `Idempotency-Key`, `If-Match` and the WebSocket upgrade, and that forwards the real client IP.
- **Moving to a new domain:** add the new domain and the new OAuth redirect first, test it, and only then retire the old one. **Removing a domain from an environment locks that live host out immediately.** Guest identities are per-origin: a new domain starts new guests.
- Send `Origin` only as the browser sets it. Never fake it in game code.
Docs: [Architecture and hosting](https://open-play.b4klabs.dev/docs/en/getting-started/architecture-and-hosting.html), [Environments and domains](https://open-play.b4klabs.dev/docs/en/panel/game-settings-environments.html).
## 5. Player data (cloud save)
- Use the generic per-player key-value store; **do not build a game-specific save server**. The value is an opaque JSON object the server never interprets; your game owns the format and its version field.
- Limits: key `^[a-z0-9][a-z0-9._-]{0,63}$`, at most 16 keys per player, 64 KiB per value, 60 writes per minute per player. Store large integers as strings.
- **There is no blind write.** Every `PUT` sends exactly one of `If-None-Match: *` (create) or `If-Match: "<version>"` (update). On `409 version_conflict` read the remote record, merge it with your own rule, retry. `428` means a precondition header is missing.
- Batch saves (on level end, on pause, on a timer), not every frame. Keep a local copy and reconcile on sign-in; a guest-to-Google link keeps the same player.
- Never write personal data you do not need. Player data is erased with the account.
Docs: [Player data](https://open-play.b4klabs.dev/docs/en/guides/player-data.html), [Idempotency and concurrency](https://open-play.b4klabs.dev/docs/en/reference/idempotency-and-concurrency.html).
## 6. Plugins (game-specific server logic)
- Call plugins only through **generated bindings**, never hand-written DTOs or copied field names. Get the schema document from `GET /v1/plugins/<PLUGIN_ID>/schema` and generate bindings from it.
- **Pin** `pluginId`, `schemaSha256`, `protocolVersion`, `codecProfile` and the plugin version in your release manifest. A mismatch must **fail closed**: show an "update the game" screen, never talk to the server with the wrong fields. After every server update compare the live digest with the pin (at startup or in CI).
- Every command carries an `Idempotency-Key`, generated **once** per logical action and reused on retries. If the outcome is uncertain (timeout, drop), resend with the **same key and body** or ask the result query; never generate a new key for the same action.
- Retry only when it is safe: idempotent calls on network errors, `429`, `503`, with exponential backoff and jitter, and honor `Retry-After`. Never retry business rejections (`422`).
- `int64` values travel as decimal strings. Send `If-Match` / `expectedVersion` when writing a shared resource; on `409` re-read first.
- If a plugin call suddenly returns `403`, check that the plugin is **enabled for that environment** and that the enabled version matches the server version. A server version bump needs its grant switched too.
Docs: [Plugins overview](https://open-play.b4klabs.dev/docs/en/plugins/overview.html), [Bindings and pinning](https://open-play.b4klabs.dev/docs/en/plugins/bindings.html), [Calling plugins](https://open-play.b4klabs.dev/docs/en/plugins/calling-plugins.html).
## 7. Realtime
- Use the SDK realtime client; if you implement the protocol yourself follow it exactly (single-use ticket, `Origin`, subprotocol).
- Ask for a **new ticket on every connection**. On a drop reconnect with exponential backoff, `Join` again with the last cursor (durable streams) or without one (ephemeral state), and on `ResyncRequired` drop the cursor and join fresh.
- Unacknowledged inputs are **not replayed**; continue from the latest snapshot. Pipeline inputs inside the SDK window (default 4 in flight); one input per round trip makes fast games feel laggy on real networks.
- Keep the frame rate below 40 per second; at most 4 sockets per player. Handle idle close (`4100`) with a "Continue" screen. An open socket does not keep the session alive.
- Notification and chat sockets cannot be opened straight from a browser; use inbox polling or REST.
Docs: [Realtime overview](https://open-play.b4klabs.dev/docs/en/realtime/overview.html), [Protocol](https://open-play.b4klabs.dev/docs/en/realtime/protocol.html), [Reconnect and limits](https://open-play.b4klabs.dev/docs/en/realtime/reconnect-and-limits.html).
## 8. Caching and deploys
- Serve your game's JavaScript modules and config (including the file that holds the environment id) with `Cache-Control: no-cache`, or content-hash the filenames. A stale config file after a deploy silently runs the old integration.
- If a CDN sits in front, set it to **respect existing headers** for HTML, JS and CSS. A CDN default browser TTL can override your `no-cache` and keep old code for hours.
- Never put a CDN cache in front of API responses that carry a cookie or `Authorization`.
- Deploy order when Open Play changes a plugin: new server and grant, then client with regenerated bindings. Pin the SDK version exactly and upgrade from the changelog.
Docs: [Production checklist](https://open-play.b4klabs.dev/docs/en/operations/production-checklist.html), [Versioning](https://open-play.b4klabs.dev/docs/en/reference/versioning.html).
## 9. Errors and rate limits
- Core endpoints return **plain-text** errors; plugin endpoints return a JSON envelope (`code`, `message`, `retryable`, `requestId`). Decide by status code and JSON `code`, **never by parsing message text**. Log `requestId` / `x-request-id`.
- There is no general per-key quota, but identity endpoints are limited per client IP (for example 30 guest sign-ins per minute). Behind a proxy or a shared network (NAT, a hosting platform's shared egress) many players share one IP: forward the real client IP.
- Back off on `429`, `503` and network errors; wait `Retry-After` when present. Never loop on `401`, `403` or `422`.
Docs: [Errors](https://open-play.b4klabs.dev/docs/en/reference/errors.html), [Rate limits](https://open-play.b4klabs.dev/docs/en/reference/rate-limits.html).
## 10. Testing checklist (before you call it done)
1. Fresh browser profile: guest sign-in, play, reload, same player and progress.
2. Guest upgraded to Google keeps progress; a second browser signs in to the same account and loads the save.
3. Two tabs open: no CSRF `403` storm; sign-out in one tab is handled in the other.
4. Same command sent twice with the same `Idempotency-Key` gives one effect.
5. Two devices write the same player-data key: the `409` is merged, nothing is lost.
6. Kill the network mid-command and mid-socket: the game recovers without duplicate effects.
7. Prod build points at the prod environment id and prod origin; dev at dev. No secrets in the bundle (search it).
8. Run these in a **headless browser or test harness** you control; do not test against another team's environment or real player data.
## Do
- Look up docs through MCP first; link the page you used in the PR description.
- Keep Open Play calls in one module/service layer, one writer per feature (never write the same state both locally and through Open Play).
- Handle every SDK session state, including `reauthRequired`, `banned` and `logoutUncertain`.
- Add a test with each integration change; report failing checks, never skip them.
## Don't
- Do not guess endpoints, headers, field names or error codes.
- Do not put a server key, token or cookie in client code, tests, logs or commits.
- Do not use `latest` or git/vendored copies of the SDK; do not hand-copy DTOs.
- Do not bypass CSRF, `Origin` or idempotency checks "just for testing".
- Do not remove a domain, redirect URI or environment that a live host still uses.
- Do not store save data in browser storage alone once the player can sign in.
- Do not write retry loops without backoff, or retry non-idempotent calls with new keys.
- Do not run production deploys or destructive operations unless the user explicitly asks.
Next step: Quickstart and the Production checklist.