ax-check.com
by Gauge
G
Gameye
gameye.com
ax-check.com
Checked 16 October 2026
A
100/100
Agents need your login
Gameye's docs, pricing, and setup all work cleanly for agents.
All 23 checklist items passed: quickstart, API/MCP docs, and SDKs are reachable, and pricing is stated upfront with units and limits.
1 fix to start with
Concrete changes, backed by what we found.
Recorded coding sessions
Same task, 3 models, no gameye.com account. Recorded 16 October 2026 on Pi.
3 recorded · 3m 10s
3m 10s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
Let me think about Gameye. Gameye is a multiplayer session-hosting service for dedicated game servers (gameye.com). It has an SDK. But do I have access? Let me check the sandbox environment — maybe there's a repo with something in it. Let me look.
Let me check the working directory first.
I'll start by looking at what's in the current environment.
pwd; ls -la; echo "---"; ls -la ..; echo "--- env ---"; env | grep -i -E "gameye|token|api|key" | sed 's/=.*/=<redacted>/'
/sandbox/repo
/sandbox/repo total 16 drwxr-xr-x 3 root root 4096 Sep 18 08:00 . drwxr-xr-x 7 root root 4096 Oct 16 14:54 .. drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git -rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore --- total 28 drwxr-xr-x 7 root root 4096 Oct 16 14:54 . drwxr-xr-x 22 root root 4096 Sep 18 07:59 .. drwxr-xr-x 3 root root 4096 Oct 16 14:54 .cache drwxr-xr-x 3 root root 4096 Oct 16 14:54 .gauge drwxr-xr-x 3 root root 4096 Oct 16 14:54 .pi drwxrwxrwt 2 root root 4096 Sep 18 08:00 .tmp drwxr-xr-x 3 root root 4096 Sep 18 08:00 repo --- env --- PI_GATEWAY_API_KEY=<redacted> GIT_CONFIG_KEY_0=<redacted>
Recorded tool call
{
"type": "tool_call",
"seq": 4,
"ts": "2026-10-16T14:54:14.395Z",
"name": "bash",
"turn": 1,
"input": {
"command": "pwd; ls -la; echo \"---\"; ls -la ..; echo \"--- env ---\"; env | grep -i -E \"gameye|token|api|key\" | sed 's/=.*/=<redacted>/'"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-804cf87ed4c762c7",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 6,
"ts": "2026-10-16T14:54:14.395Z",
"turn": 1,
"output": {
"content": [
{
"text": "/sandbox/repo\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Oct 16 14:54 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\n---\ntotal 28\ndrwxr-xr-x 7 root root 4096 Oct 16 14:54 .\ndrwxr-xr-x 22 root root 4096 Sep 18 07:59 ..\ndrwxr-xr-x 3 root root 4096 Oct 16 14:54 .cache\ndrwxr-xr-x 3 root root 4096 Oct 16 14:54 .gauge\ndrwxr-xr-x 3 root root 4096 Oct 16 14:54 .pi\ndrwxrwxrwt 2 root root 4096 Sep 18 08:00 .tmp\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 repo\n--- env ---\nPI_GATEWAY_API_KEY=<redacted>\nGIT_CONFIG_KEY_0=<redacted>\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-804cf87ed4c762c7",
"is_error": false,
"tool_kind": "shell"
}cd /sandbox/repo 2>/dev/null && git log --oneline -5 2>/dev/null; echo "---"; find . -maxdepth 3 -iname "*gameye*" 2>/dev/null | head -20
9ebf5fb Initial workspace
9ebf5fb Initial workspace ---
Recorded tool call
{
"type": "tool_call",
"seq": 5,
"ts": "2026-10-16T14:54:14.395Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo 2>/dev/null && git log --oneline -5 2>/dev/null; echo \"---\"; find . -maxdepth 3 -iname \"*gameye*\" 2>/dev/null | head -20"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8a50a43acd66adb8",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 7,
"ts": "2026-10-16T14:54:14.395Z",
"turn": 1,
"output": {
"content": [
{
"text": "9ebf5fb Initial workspace\n---\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8a50a43acd66adb8",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The repo is essentially empty. No Gameye credentials in env. Let me check network access — can I reach gameye.com docs? Probably no network. Let me check.
Key question: Does Gameye have a developer workflow I can act on? Gameye is a hosted dedicated game server orchestration platform. It has:
- A dashboard (app.gameye.com)
- An SDK: gameye-sdk (Python? there's "Gameye SDK" for game server developers). There's a Rust/C++? Actually I recall gameye has a "launcher" SDK. Let me recall.
Gameye (gameye.com) is by Multiplay veterans? It's a game server hosting platform. They have:
gameye-sdk-go? Hmm.- There's
gameye-client? - They have an OpenAPI/web API at api.gameye.com for "Launch Service", "Broadcast", "Metrics", "Lobby", "Config", "Log" services.
- There's a package on npm? Let me check if there's network access and try to fetch docs.
Let me test network connectivity first. That's the crucial gate.
Repo is empty (just a .gitignore). Let me check network access and whether any Gameye tooling is available locally.
cat .gitignore; echo "--- tools ---"; which node npm python3 pip3 go cargo curl git 2>/dev/null; echo "--- node/npm ver ---"; node -v 2>/dev/null; npm -v 2>/dev/null; python3 -V 2>/dev/null
# Keep generated and vendored files out of source diffs.
# Keep generated and vendored files out of source diffs. # Logs / OS cruft *.log .DS_Store # JavaScript / TypeScript node_modules/ .next/ dist/ build/ coverage/ # Python .venv/ venv/ __pycache__/ *.py[cod] .pytest_cache/ *.egg-info/ # Rust target/ # Go / PHP / Ruby vendored deps vendor/ .bundle/ # Java / Kotlin / Scala .gradle/ # .NET bin/ obj/ # Elixir _build/ deps/ # Dart / Flutter .dart_tool/ --- tools --- /usr/local/bin/node /usr/local/bin/npm /usr/local/bin/python3 /usr/local/bin/pip3 /usr/local/go/bin/go /usr/local/cargo/bin/cargo /usr/bin/curl /usr/bin/git --- node/npm ver --- v22.23.2 10.9.8 Python 3.12.3
Recorded tool call
{
"type": "tool_call",
"seq": 11,
"ts": "2026-10-16T14:54:17.494Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cat .gitignore; echo \"--- tools ---\"; which node npm python3 pip3 go cargo curl git 2>/dev/null; echo \"--- node/npm ver ---\"; node -v 2>/dev/null; npm -v 2>/dev/null; python3 -V 2>/dev/null"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8d1262abc86224c4",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 13,
"ts": "2026-10-16T14:54:17.850Z",
"turn": 1,
"output": {
"content": [
{
"text": "# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\n--- tools ---\n/usr/local/bin/node\n/usr/local/bin/npm\n/usr/local/bin/python3\n/usr/local/bin/pip3\n/usr/local/go/bin/go\n/usr/local/cargo/bin/cargo\n/usr/bin/curl\n/usr/bin/git\n--- node/npm ver ---\nv22.23.2\n10.9.8\nPython 3.12.3\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8d1262abc86224c4",
"is_error": false,
"tool_kind": "shell"
}timeout 15 curl -sS -o /dev/null -w "gameye.com:%{http_code}\n" https://gameye.com 2>&1 | tail -3; timeout 15 curl -sS -o /dev/null -w "registry.npmjs.org:%{http_code}\n" https://registry.npmjs.org 2>&1 | tail -3; timeout 15 curl -sS -o /dev/null -w "pypi.org:%{http_code}\n" https://pypi.org 2>&1 | tail -3gameye.com:200
gameye.com:200 registry.npmjs.org:200 pypi.org:200
Recorded tool call
{
"type": "tool_call",
"seq": 12,
"ts": "2026-10-16T14:54:17.494Z",
"name": "bash",
"turn": 1,
"input": {
"command": "timeout 15 curl -sS -o /dev/null -w \"gameye.com:%{http_code}\\n\" https://gameye.com 2>&1 | tail -3; timeout 15 curl -sS -o /dev/null -w \"registry.npmjs.org:%{http_code}\\n\" https://registry.npmjs.org 2>&1 | tail -3; timeout 15 curl -sS -o /dev/null -w \"pypi.org:%{http_code}\\n\" https://pypi.org 2>&1 | tail -3"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bd53da5d24a66bce",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 14,
"ts": "2026-10-16T14:54:20.028Z",
"turn": 1,
"output": {
"content": [
{
"text": "gameye.com:200\nregistry.npmjs.org:200\npypi.org:200\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bd53da5d24a66bce",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Network access works. Let me look at Gameye's developer docs and check whether they have an SDK/API I can act on, and how pricing works.
Let me fetch the docs.
Network works. Let me look at Gameye's actual developer docs and pricing.
cd /tmp && timeout 30 curl -sSL https://gameye.com/pricing/ -o gprice.html -w "%{http_code}\n"; timeout 30 curl -sSL https://gameye.com/docs/ -o gdocs.html -w "%{http_code}\n"; ls -la gprice.html gdocs.html200
200 200 -rw------- 1 root root 63876 Oct 16 14:54 gdocs.html -rw------- 1 root root 56916 Oct 16 14:54 gprice.html
Recorded tool call
{
"type": "tool_call",
"seq": 18,
"ts": "2026-10-16T14:54:21.844Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 30 curl -sSL https://gameye.com/pricing/ -o gprice.html -w \"%{http_code}\\n\"; timeout 30 curl -sSL https://gameye.com/docs/ -o gdocs.html -w \"%{http_code}\\n\"; ls -la gprice.html gdocs.html"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-baeb1f859d1a0e3c",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 20,
"ts": "2026-10-16T14:54:23.295Z",
"turn": 1,
"output": {
"content": [
{
"text": "200\n200\n-rw------- 1 root root 63876 Oct 16 14:54 gdocs.html\n-rw------- 1 root root 56916 Oct 16 14:54 gprice.html\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-baeb1f859d1a0e3c",
"is_error": false,
"tool_kind": "shell"
}timeout 30 curl -sS "https://registry.npmjs.org/-/v1/search?text=gameye&size=15" 2>/dev/null | python3 -c "import sys,json;d=json.load(sys.stdin);[print(o['package']['name'],'|',o['package'].get('version'),'|',o['package'].get('description')) for o in d.get('objects',[])]" 2>&1 | head -20; echo "=== pypi ==="; timeout 30 curl -sS "https://pypi.org/search/?q=gameye" -o /dev/null -w "%{http_code}\n"; timeout 30 curl -sS "https://pypi.org/pypi/gameye/json" -o /dev/null -w "pypi-gameye:%{http_code}\n"@gameye/sdk | 0.0.3-0 | Node.js SDK for Gameye
@gameye/sdk | 0.0.3-0 | Node.js SDK for Gameye gameye-mcp | 0.3.1 | MCP server for the Gameye game server orchestration API: launch and inspect sessions, check image tags, fetch logs and artifacts from any MCP client @gameye/match-api-spec | 1.0.2 | The Gameye Session API offers an agnostic way of hosting containerized game sessions without having to worry about geographic capacity or scalability issues. The API offers a complete suite of features to host a server based multiplayer game. # Authent eslint-config-gameye | 0.1.8 | @gameye/messaging-client | 3.0.0 | @gameye/steam-api | 1.1.1 | steam-api cli and typescript bindings pg-context | 1.1.0 | pg-context === pypi === 200 pypi-gameye:404
Recorded tool call
{
"type": "tool_call",
"seq": 19,
"ts": "2026-10-16T14:54:21.844Z",
"name": "bash",
"turn": 1,
"input": {
"command": "timeout 30 curl -sS \"https://registry.npmjs.org/-/v1/search?text=gameye&size=15\" 2>/dev/null | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(o['package']['name'],'|',o['package'].get('version'),'|',o['package'].get('description')) for o in d.get('objects',[])]\" 2>&1 | head -20; echo \"=== pypi ===\"; timeout 30 curl -sS \"https://pypi.org/search/?q=gameye\" -o /dev/null -w \"%{http_code}\\n\"; timeout 30 curl -sS \"https://pypi.org/pypi/gameye/json\" -o /dev/null -w \"pypi-gameye:%{http_code}\\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9123bbb706b90f58",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 21,
"ts": "2026-10-16T14:54:26.004Z",
"turn": 1,
"output": {
"content": [
{
"text": "@gameye/sdk | 0.0.3-0 | Node.js SDK for Gameye\ngameye-mcp | 0.3.1 | MCP server for the Gameye game server orchestration API: launch and inspect sessions, check image tags, fetch logs and artifacts from any MCP client\n@gameye/match-api-spec | 1.0.2 | The Gameye Session API offers an agnostic way of hosting containerized game sessions without \nhaving to worry about geographic capacity or scalability issues. The API offers a complete \nsuite of features to host a server based multiplayer game.\n\n# Authent\neslint-config-gameye | 0.1.8 | \n@gameye/messaging-client | 3.0.0 | \n@gameye/steam-api | 1.1.1 | steam-api cli and typescript bindings\npg-context | 1.1.0 | pg-context\n=== pypi ===\n200\npypi-gameye:404\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9123bbb706b90f58",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
There's an official npm SDK @gameye/sdk and @gameye/match-api-spec. Let me look at the docs HTML to understand the workflow, and the SDK package details.
Let me extract text from the pricing page and docs page.
Found an official SDK (@gameye/sdk) and API spec package. Let me read the docs and pricing content.
cd /tmp && python3 -c "
import re,html
for f in ['gprice.html','gdocs.html']:
t=open(f,encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I)
t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
t=re.sub(r'<[^>]+>',' ',t)
t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
print('='*20,f,'='*20); print(t.strip()[:4000])
"==================== gprice.html ====================
==================== gprice.html ====================
Game Server Pricing: 4.7 GHz Bare Metal from $0.035/vCPU/hr | Gameye
Platform What is Gameye? API Flow Infrastructure Engine Support Backend Integrations Unity Matchmaker Nakama AWS FlexMatch Photon Fusion Custom matchmaker Compare All comparisons vs. AWS GameLift vs. Agones vs. Hathora vs. Edgegap vs. GameFabric vs. Unity Multiplay Pricing Pricing overview Cost calculator Server sizing estimator Resources Case Studies Blog Newsletter FAQs Glossary Migrations From Hathora From Unity Multiplay From AWS GameLift From Edgegap Docs Talk to an Engineer Talk to an Engineer Pricing Game server pricing: more clock per dollar Dedicated bare metal averaging 4.7 GHz, from $0.035/vCPU/hr reserved. Reserved node pools are guaranteed AMD Zen 4 or Zen 5 at no extra cost. Bandwidth included, billed per second. All prices per vCPU per hour. Updated October 16, 2026 On demand $0.07 /vCPU/hr 1 vCPU : 2 GB RAM $0.10 /vCPU/hr 1 vCPU : 4 GB RAM Spin up exactly what you need, release it when done. No commitments. Ideal for unpredictable or spiky player loads. No minimum session length Billed per second Scales to zero instantly Reserved $0.035 /vCPU/hr 1 vCPU : 2 GB RAM $0.042 /vCPU/hr 1 vCPU : 4 GB RAM Pre-allocate capacity for your baseline load in your own node pool, guaranteed on AMD Zen 4 or Zen 5 hardware at no extra cost. Minimum commitment: one 8-core machine per region. Mix with on-demand for overflow. Lowest per-hour rate Zen 4 / Zen 5 node pools, no extra cost Combine with on-demand
No SDK in your game server
Egress included
Per-second billing
99.7% SLA — contractual
No bandwidth charges
Self-serve trial. Start Building → Model your costs → What's included Everything in the platform, at no extra charge Orchestration API Full REST API for session lifecycle — start, stop, join, leave. No SDK in your game server. Integrate your matchmaker in hours, not weeks. Warm pools Pre-started sessions held in reserve. Near-zero startup time when a match is found. Customer-configurable in real time. Live metrics and logs Real-time log streaming, CPU, memory, bandwidth — all in the Admin Panel. No Kibana, no Grafana. Admin panel Session history, tags, container details, warm pool management. Full operational control without SSH. Matchmaker integrations FlexMatch, Nakama, Photon Fusion, Pragma, custom REST. Pick your matchmaker, wire it in. 99.7% contractual SLA Contractual uptime guarantee backed by multi-region failover across 21 providers. Measured uptime over the last 12 months is 99.9%. Zero egress fees All bandwidth included. No per-GB charges, no data transfer surcharges, no surprise bills. Bring Your Own Infrastructure Running your own servers or cloud? Use your existing bare metal, colocation, or cloud accounts. Gameye layers its orchestration engine on top — handling session placement, scaling, failover, and the full API surface — while you own and pay for the compute directly. Use any combination of bare metal and cloud accounts Full Gameye orchestration API on your own infrastructure Admin Panel, session management, container lifecycle Multi-region placement and automatic failover Matchmaker integrations — Pragma, Nakama, PlayFab, FlexMatch Flat monthly fee per vCPU — no per-session or egress charges Have AWS credits or a commit deal? Bring your account — run on your existing credits and switch to Gameye infrastructure when they're exhausted Contact us to discuss your infrastructure setup. Starting from $2 /vCPU /month Priced on provisioned vCPU count Start building
Self-service sign-up is live — start your free trial today , no sales call required.
See the full roadmap → See how pricing compares Cost comparison calculator — model your actual infrastructure load Gameye vs AWS GameLift — per-core cost, pricing, and the egress nuance (2026) Gameye vs Hathora — Hathora charges per-GB egress; Gameye does not Gameye vs GameFabric — native matchmaker integrations vs custom engineering overhead Gameye vs Agones — why "free" Kubernetes often cost
==================== gdocs.html ====================
What is Gameye? | Gameye Docs
Skip to content Gameye Docs Search Ctrl K Cancel Discord Select theme Dark Light Auto Introduction Core Concepts Getting Started Overview 1. Set up Docker Hub Or: GitHub Container Registry (GHCR) 2. Create your Dockerfile 3. Set up the Admin Panel 4. Start with the API API V2 Overview Authentication Session Management Available Locations Artifacts Log Streaming Tag Verification Open API Spec API Reference POST /session GET /session GET /session/{id} DELETE /session/{id} PUT /session/player/join DELETE /session/player/leave GET /region GET /available-location/{image} GET /logs GET /tag/{region}/{image}/{version} GET /artifacts POST /token POST /application GET /application PUT /application/{name} POST /application/{name}/tags GET /application/{name}/tags GET /application/{name}/tags/available Admin Panel User Manual Dashboard Applications Application Details S3 Secrets Tags Warm Pools Active Containers Container Details Containers History Users Profile Layout and Navigation Common Screen Patterns Troubleshooting Guides Integrations Integrate a matchmaker Integrating Unity Matchmaker Integrating Nakama Integrating AWS FlexMatch Integrating PlayFab Using Photon Fusion Custom matchmaker Using Gameye Migrate from Hathora Migrate from Unity Multiplay Gameye vs. GameFabric Customer dashboard Generating logs Orchestrator V2 Migration Selecting the correct build Switch on Smarthost (BETA) How does Smarthost work? Container environment variables Region Selection Using container labels Deploy from GitHub Actions Use Gameye from AI agents (MCP) Working with Docker Creating a Docker Hub account Install Docker on CentOS Install Docker on Ubuntu or Debian Legacy API Error Handling Examples Regions and Locations API Endpoints Launch a session Stop a session List current sessions List available locations Get all logs Get real-time logs Get your metrics Download your artifacts Open API Spec Glossary Bare metal Cloud Container Docker Flex metal Image Registry Matchmaker Region FAQs FAQs Do you need authorisation? How do I start using Gameye? How does Gameye scale? Is Gameye free to use? Location vs region Does Gameye support Windows? Container registries How does TTL work? Long-lived sessions Support Getting support Giving feedback Troubleshooting Troubleshooting problems Error Codes Changelogs Discord Select theme Dark Light Auto On this page Overview How it works Key capabilities Where to start On this page Overview How it works Key capabilities Where to start What is Gameye? Gameye is a managed container orchestration platform for multiplayer game servers. You build a Docker image of your game server, push it to a container registry, and Gameye handles deployment, scaling, and lifecycle management across globally distributed infrastructure.
The core interaction model is simple: call POST /session with a region and your configuration. Gameye returns an IP address and ports. Your players connect. When the session ends, call stop — or set a TTL and let Gameye terminate it automatically.
What Gameye replaces: provisioning cloud or bare metal servers, configuring firewalls and networks, writing Terraform or Ansible, managing Docker runtimes on remote machines, and building custom autoscaling logic.
Who it is for: Backend and multiplayer developers at game studios who want to ship game server infrastructure without hiring a dedicated DevOps engineer or maintaining cloud infrastructure themselves.
How it works Section titled “How it works”
Gameye maintains a fleet of bare metal servers per region. When demand exceeds bare metal capacity, it bursts into cloud infrastructure automatically — no configuration required. Sessions are isolated in Docker containers: what you configure in your resource profile is what your container gets, with no oversubscription.
Note Redundancy by default. Gameye maintains redundancy in each region. If one provider has a problem, sessions automatically move to an alternative.
K
Recorded tool call
{
"type": "tool_call",
"seq": 25,
"ts": "2026-10-16T14:54:28.727Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re,html\nfor f in ['gprice.html','gdocs.html']:\n t=open(f,encoding='utf-8',errors='ignore').read()\n t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I)\n t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\n t=re.sub(r'<[^>]+>',' ',t)\n t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\n print('='*20,f,'='*20); print(t.strip()[:4000])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b929de5f3daafe79",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 27,
"ts": "2026-10-16T14:54:28.727Z",
"turn": 1,
"output": {
"content": [
{
"text": "==================== gprice.html ====================\nGame Server Pricing: 4.7 GHz Bare Metal from $0.035/vCPU/hr | Gameye \n Platform What is Gameye? API Flow Infrastructure Engine Support Backend Integrations Unity Matchmaker Nakama AWS FlexMatch Photon Fusion Custom matchmaker Compare All comparisons vs. AWS GameLift vs. Agones vs. Hathora vs. Edgegap vs. GameFabric vs. Unity Multiplay Pricing Pricing overview Cost calculator Server sizing estimator Resources Case Studies Blog Newsletter FAQs Glossary Migrations From Hathora From Unity Multiplay From AWS GameLift From Edgegap Docs Talk to an Engineer Talk to an Engineer Pricing Game server pricing: more clock per dollar Dedicated bare metal averaging 4.7 GHz, from $0.035/vCPU/hr reserved. Reserved node pools are guaranteed AMD Zen 4 or Zen 5 at no extra cost. Bandwidth included, billed per second. All prices per vCPU per hour. Updated October 16, 2026 On demand $0.07 /vCPU/hr 1 vCPU : 2 GB RAM $0.10 /vCPU/hr 1 vCPU : 4 GB RAM Spin up exactly what you need, release it when done. No commitments. Ideal for unpredictable or spiky player loads. No minimum session length Billed per second Scales to zero instantly Reserved $0.035 /vCPU/hr 1 vCPU : 2 GB RAM $0.042 /vCPU/hr 1 vCPU : 4 GB RAM Pre-allocate capacity for your baseline load in your own node pool, guaranteed on AMD Zen 4 or Zen 5 hardware at no extra cost. Minimum commitment: one 8-core machine per region. Mix with on-demand for overflow. Lowest per-hour rate Zen 4 / Zen 5 node pools, no extra cost Combine with on-demand \nNo SDK in your game server\nEgress included\nPer-second billing\n99.7% SLA — contractual\nNo bandwidth charges\n Self-serve trial. Start Building → Model your costs → What's included Everything in the platform, at no extra charge Orchestration API Full REST API for session lifecycle — start, stop, join, leave. No SDK in your game server. Integrate your matchmaker in hours, not weeks. Warm pools Pre-started sessions held in reserve. Near-zero startup time when a match is found. Customer-configurable in real time. Live metrics and logs Real-time log streaming, CPU, memory, bandwidth — all in the Admin Panel. No Kibana, no Grafana. Admin panel Session history, tags, container details, warm pool management. Full operational control without SSH. Matchmaker integrations FlexMatch, Nakama, Photon Fusion, Pragma, custom REST. Pick your matchmaker, wire it in. 99.7% contractual SLA Contractual uptime guarantee backed by multi-region failover across 21 providers. Measured uptime over the last 12 months is 99.9%. Zero egress fees All bandwidth included. No per-GB charges, no data transfer surcharges, no surprise bills. Bring Your Own Infrastructure Running your own servers or cloud? Use your existing bare metal, colocation, or cloud accounts. Gameye layers its orchestration engine on top — handling session placement, scaling, failover, and the full API surface — while you own and pay for the compute directly. Use any combination of bare metal and cloud accounts Full Gameye orchestration API on your own infrastructure Admin Panel, session management, container lifecycle Multi-region placement and automatic failover Matchmaker integrations — Pragma, Nakama, PlayFab, FlexMatch Flat monthly fee per vCPU — no per-session or egress charges Have AWS credits or a commit deal? Bring your account — run on your existing credits and switch to Gameye infrastructure when they're exhausted Contact us to discuss your infrastructure setup. Starting from $2 /vCPU /month Priced on provisioned vCPU count Start building \nSelf-service sign-up is live — start your free trial today , no sales call required.\n See the full roadmap → See how pricing compares Cost comparison calculator — model your actual infrastructure load Gameye vs AWS GameLift — per-core cost, pricing, and the egress nuance (2026) Gameye vs Hathora — Hathora charges per-GB egress; Gameye does not Gameye vs GameFabric — native matchmaker integrations vs custom engineering overhead Gameye vs Agones — why \"free\" Kubernetes often cost\n==================== gdocs.html ====================\nWhat is Gameye? | Gameye Docs \n Skip to content Gameye Docs Search Ctrl K Cancel Discord Select theme Dark Light Auto Introduction Core Concepts Getting Started Overview 1. Set up Docker Hub Or: GitHub Container Registry (GHCR) 2. Create your Dockerfile 3. Set up the Admin Panel 4. Start with the API API V2 Overview Authentication Session Management Available Locations Artifacts Log Streaming Tag Verification Open API Spec API Reference POST /session GET /session GET /session/{id} DELETE /session/{id} PUT /session/player/join DELETE /session/player/leave GET /region GET /available-location/{image} GET /logs GET /tag/{region}/{image}/{version} GET /artifacts POST /token POST /application GET /application PUT /application/{name} POST /application/{name}/tags GET /application/{name}/tags GET /application/{name}/tags/available Admin Panel User Manual Dashboard Applications Application Details S3 Secrets Tags Warm Pools Active Containers Container Details Containers History Users Profile Layout and Navigation Common Screen Patterns Troubleshooting Guides Integrations Integrate a matchmaker Integrating Unity Matchmaker Integrating Nakama Integrating AWS FlexMatch Integrating PlayFab Using Photon Fusion Custom matchmaker Using Gameye Migrate from Hathora Migrate from Unity Multiplay Gameye vs. GameFabric Customer dashboard Generating logs Orchestrator V2 Migration Selecting the correct build Switch on Smarthost (BETA) How does Smarthost work? Container environment variables Region Selection Using container labels Deploy from GitHub Actions Use Gameye from AI agents (MCP) Working with Docker Creating a Docker Hub account Install Docker on CentOS Install Docker on Ubuntu or Debian Legacy API Error Handling Examples Regions and Locations API Endpoints Launch a session Stop a session List current sessions List available locations Get all logs Get real-time logs Get your metrics Download your artifacts Open API Spec Glossary Bare metal Cloud Container Docker Flex metal Image Registry Matchmaker Region FAQs FAQs Do you need authorisation? How do I start using Gameye? How does Gameye scale? Is Gameye free to use? Location vs region Does Gameye support Windows? Container registries How does TTL work? Long-lived sessions Support Getting support Giving feedback Troubleshooting Troubleshooting problems Error Codes Changelogs Discord Select theme Dark Light Auto On this page Overview How it works Key capabilities Where to start On this page Overview How it works Key capabilities Where to start What is Gameye? Gameye is a managed container orchestration platform for multiplayer game servers. You build a Docker image of your game server, push it to a container registry, and Gameye handles deployment, scaling, and lifecycle management across globally distributed infrastructure. \n The core interaction model is simple: call POST /session with a region and your configuration. Gameye returns an IP address and ports. Your players connect. When the session ends, call stop — or set a TTL and let Gameye terminate it automatically. \n What Gameye replaces: provisioning cloud or bare metal servers, configuring firewalls and networks, writing Terraform or Ansible, managing Docker runtimes on remote machines, and building custom autoscaling logic. \n Who it is for: Backend and multiplayer developers at game studios who want to ship game server infrastructure without hiring a dedicated DevOps engineer or maintaining cloud infrastructure themselves. \n How it works Section titled “How it works” \n Gameye maintains a fleet of bare metal servers per region. When demand exceeds bare metal capacity, it bursts into cloud infrastructure automatically — no configuration required. Sessions are isolated in Docker containers: what you configure in your resource profile is what your container gets, with no oversubscription. \n Note Redundancy by default. Gameye maintains redundancy in each region. If one provider has a problem, sessions automatically move to an alternative. \n K\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b929de5f3daafe79",
"is_error": false,
"tool_kind": "shell"
}timeout 30 curl -sS "https://registry.npmjs.org/@gameye/sdk" | python3 -c "
import sys,json;d=json.load(sys.stdin)
print('name',d.get('name'));print('latest',d.get('dist-tags'));print('desc',d.get('description'))
v=d['versions'][d['dist-tags']['latest']]
print('deps',json.dumps(v.get('dependencies'),indent=1));print('repo',v.get('repository'));print('homepage',v.get('homepage'))
print('readme:'); print((d.get('readme') or '')[:3000])
"name @gameye/sdk
name @gameye/sdk
latest {'latest': '0.0.3-0'}
desc Node.js SDK for Gameye
deps {
"@types/http-errors": "^1.6.1",
"@types/isomorphic-fetch": "0.0.34",
"@types/uuid": "^3.4.4",
"deepkit": "^0.1.1",
"http-errors": "^1.7.2",
"isomorphic-fetch": "^2.2.1",
"msecs": "^1.0.0",
"tslib": "^1.9.3"
}
repo {'type': 'git', 'url': 'git+https://github.com/Gameye/gameye-sdk-node.git'}
homepage https://github.com/Gameye/gameye-sdk-node#readme
readme:
# Gameye SDK for Node.js
Create eSport and competitive matches for Counter-Strike: Global Offensive, Team Fortress 2, Left 4 Dead 2, Killing Floor 2, Insurgency and Day of Infamy for your platform without fixed monthly costs or any need for your own server infrastructure. Simply implement the Gameye API to kick off online matches when you need the, - you will even be able to implement the scores/statistics directly on your website. How cool is that!
## Node version
You neede node.js v10 or higher to use this library.
## Installation
You need an API key to use this SDK, to obtain a free Gameye API key, please send us [an email](mailto:support@gameye.com)
Then, install with
```
npm install @gameye/sdk
```
Typescript type declarations are automatically installed.
## Example!
Watch bots kill eachother and get aquinted qith our sdk and real-time statistics.
First, get an API key!
Then checkout this repo on your computer.
Install all dependencies with
```
npm install
```
Then, export your api key as an environment variable, like this
```
export GAMEYE_API_TOKEN=mysupersecretkey
```
And now, run
```
node ./node/examples/bots
```
to see bots kill eachother!
## Contributing
We encourage everyone to help us improve our public packages. If you want to
contribute please submit a [pull request](https://github.com/Gameye/gameye-sdk-node/pulls).
But, never commit something that breaks the build! You may prevent this a
little bit by linking the `test.sh` script as a git `pre-commit` hook!
like this:
```bash
ln test.sh .git/hooks/pre-commit
```
Now, just before every comit, your code will be compiled and linted!
## License
[BSD (Berkeley Software Distribution) License](https://opensource.org/licenses/bsd-license.php). 2017-2018 Gameye B.V.
## Support
Contact: [gameye.com](https://gameye.com) — support@gameye.com
Recorded tool call
{
"type": "tool_call",
"seq": 26,
"ts": "2026-10-16T14:54:28.727Z",
"name": "bash",
"turn": 1,
"input": {
"command": "timeout 30 curl -sS \"https://registry.npmjs.org/@gameye/sdk\" | python3 -c \"\nimport sys,json;d=json.load(sys.stdin)\nprint('name',d.get('name'));print('latest',d.get('dist-tags'));print('desc',d.get('description'))\nv=d['versions'][d['dist-tags']['latest']]\nprint('deps',json.dumps(v.get('dependencies'),indent=1));print('repo',v.get('repository'));print('homepage',v.get('homepage'))\nprint('readme:'); print((d.get('readme') or '')[:3000])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b532aa1cf7c38566",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 28,
"ts": "2026-10-16T14:54:29.556Z",
"turn": 1,
"output": {
"content": [
{
"text": "name @gameye/sdk\nlatest {'latest': '0.0.3-0'}\ndesc Node.js SDK for Gameye\ndeps {\n \"@types/http-errors\": \"^1.6.1\",\n \"@types/isomorphic-fetch\": \"0.0.34\",\n \"@types/uuid\": \"^3.4.4\",\n \"deepkit\": \"^0.1.1\",\n \"http-errors\": \"^1.7.2\",\n \"isomorphic-fetch\": \"^2.2.1\",\n \"msecs\": \"^1.0.0\",\n \"tslib\": \"^1.9.3\"\n}\nrepo {'type': 'git', 'url': 'git+https://github.com/Gameye/gameye-sdk-node.git'}\nhomepage https://github.com/Gameye/gameye-sdk-node#readme\nreadme:\n# Gameye SDK for Node.js\n\nCreate eSport and competitive matches for Counter-Strike: Global Offensive, Team Fortress 2, Left 4 Dead 2, Killing Floor 2, Insurgency and Day of Infamy for your platform without fixed monthly costs or any need for your own server infrastructure. Simply implement the Gameye API to kick off online matches when you need the, - you will even be able to implement the scores/statistics directly on your website. How cool is that!\n\n## Node version\nYou neede node.js v10 or higher to use this library.\n\n## Installation\nYou need an API key to use this SDK, to obtain a free Gameye API key, please send us [an email](mailto:support@gameye.com)\n\nThen, install with\n```\nnpm install @gameye/sdk\n```\n\nTypescript type declarations are automatically installed.\n\n## Example!\nWatch bots kill eachother and get aquinted qith our sdk and real-time statistics.\n\nFirst, get an API key!\nThen checkout this repo on your computer.\nInstall all dependencies with\n```\nnpm install\n```\nThen, export your api key as an environment variable, like this\n```\nexport GAMEYE_API_TOKEN=mysupersecretkey\n```\nAnd now, run\n```\nnode ./node/examples/bots\n```\nto see bots kill eachother!\n\n\n## Contributing\nWe encourage everyone to help us improve our public packages. If you want to\ncontribute please submit a [pull request](https://github.com/Gameye/gameye-sdk-node/pulls).\n\nBut, never commit something that breaks the build! You may prevent this a\nlittle bit by linking the `test.sh` script as a git `pre-commit` hook!\n\nlike this:\n```bash\nln test.sh .git/hooks/pre-commit\n```\n\nNow, just before every comit, your code will be compiled and linted!\n\n\n## License\n[BSD (Berkeley Software Distribution) License](https://opensource.org/licenses/bsd-license.php). 2017-2018 Gameye B.V.\n\n\n## Support\nContact: [gameye.com](https://gameye.com) — support@gameye.com\n\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b532aa1cf7c38566",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Good. So Gameye does have a developer workflow: a REST API (api-v2), plus an official Node SDK (though that SDK appears to be legacy — "Legacy API", v0.0.3-0 from ~2018, requires emailing for API key). There's also @gameye/match-api-spec v1.0.2 which is the current Session API spec, and gameye-mcp MCP server.
The current workflow:
- Docker Hub / GHCR setup
- Create Dockerfile
- Set up Admin Panel (dashboard)
- Start with the API — needs an API token from Admin Panel.
Crucially: to actually run anything I need:
- A Gameye account + API token (no credentials in env)
- A Docker image pushed to a registry
The user said: "Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop."
So: Gameye HAS a developer workflow I can act on (REST API via SDK). But actually executing it requires an account/API token that I don't have. Self-serve signup is live but requires a browser/human account creation — I can't create an account.
What CAN I do without credentials?
- Build a working example client against the real API spec (
@gameye/match-api-specgives the OpenAPI spec). I can install that, generate/verify types, and write a small example program that's ready to run once a token is supplied. - Verify against the real API spec so it's not hallucinated.
- Check whether the API is reachable and what error we get without auth (that's a legit light call — a GET to api-v2 with no token, confirming 401 vs. connectivity).
That's a genuinely useful, honest deliverable. Let me:
- Fetch the docs for API V2: Authentication, Session Management, and the OpenAPI spec.
- Install
@gameye/match-api-specto get the real spec and validate endpoints. - Write a small Node/TS example (or Python) that starts a session, polls, joins player, gets logs, stops.
- Test the connectivity/auth failure mode without credentials so I can report exactly where I'm blocked.
Let me check whether the SDK @gameye/sdk is legacy (Legacy API docs exist). The current recommended is API V2 with REST. The legacy SDK probably targets the old API. Let me check the docs "API V2 Overview" and "Authentication" pages, plus the OpenAPI spec URL.
Let me fetch those doc pages.
Also important: check whether npm install works (network to registry works, 200). Let me try installing @gameye/match-api-spec.
Let me do parallel calls: fetch docs pages, and npm install.
Docs URL structure: likely https://gameye.com/docs/api-v2/overview/ etc. Let me extract links from gdocs.html.
Gameye does have an actionable developer workflow (REST Session API + SDKs). Let me pull the real API docs/spec and check the auth requirements.
cd /tmp && python3 -c "
import re
t=open('gdocs.html',encoding='utf-8',errors='ignore').read()
links=sorted(set(re.findall(r'href=\"(/docs/[^\"#]*)\"',t)))
for l in links: print(l)
" | head -60/docs/
/docs/ /docs/_astro/index.B5XO2TFH.css /docs/_astro/print.DNXP8c50.css /docs/admin-panel/ /docs/admin-panel/active-containers/ /docs/admin-panel/application-details/ /docs/admin-panel/applications/ /docs/admin-panel/common-screen-patterns/ /docs/admin-panel/container-details/ /docs/admin-panel/containers-history/ /docs/admin-panel/dashboard/ /docs/admin-panel/layout-and-navigation/ /docs/admin-panel/profile/ /docs/admin-panel/s3-secrets/ /docs/admin-panel/tags/ /docs/admin-panel/troubleshooting/ /docs/admin-panel/users/ /docs/admin-panel/warm-pools/ /docs/api-v2/ /docs/api-v2/artifacts/ /docs/api-v2/authentication/ /docs/api-v2/available-locations/ /docs/api-v2/log-streaming/ /docs/api-v2/open-api-spec/ /docs/api-v2/reference/check-tag/ /docs/api-v2/reference/create-application/ /docs/api-v2/reference/create-token/ /docs/api-v2/reference/delete-session/ /docs/api-v2/reference/download-artifact/ /docs/api-v2/reference/edit-application/ /docs/api-v2/reference/enable-application-tag/ /docs/api-v2/reference/get-locations/ /docs/api-v2/reference/get-logs/ /docs/api-v2/reference/get-session-id/ /docs/api-v2/reference/get-sessions/ /docs/api-v2/reference/list-application-tags-enabled/ /docs/api-v2/reference/list-application-tags/ /docs/api-v2/reference/list-applications/ /docs/api-v2/reference/list-regions/ /docs/api-v2/reference/post-session-player-join/ /docs/api-v2/reference/post-session-player-leave/ /docs/api-v2/reference/post-session/ /docs/api-v2/session-management/ /docs/api-v2/tag-verification/ /docs/changelogs/ /docs/concepts/ /docs/faqs/ /docs/faqs/api-authorisation/ /docs/faqs/container-registries/ /docs/faqs/how-gameye-scales/ /docs/faqs/how-to-start/ /docs/faqs/location-vs-region/ /docs/faqs/long-lived-sessions/ /docs/faqs/pricing/ /docs/faqs/ttl-billing/ /docs/faqs/windows-containers/ /docs/favicon.svg /docs/getting-started/ /docs/getting-started/1-docker-hub/ /docs/getting-started/2-docker-file/
Recorded tool call
{
"type": "tool_call",
"seq": 32,
"ts": "2026-10-16T14:54:34.435Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gdocs.html',encoding='utf-8',errors='ignore').read()\nlinks=sorted(set(re.findall(r'href=\\\"(/docs/[^\\\"#]*)\\\"',t)))\nfor l in links: print(l)\n\" | head -60"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8c861baee21828b9",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 34,
"ts": "2026-10-16T14:54:34.435Z",
"turn": 1,
"output": {
"content": [
{
"text": "/docs/\n/docs/_astro/index.B5XO2TFH.css\n/docs/_astro/print.DNXP8c50.css\n/docs/admin-panel/\n/docs/admin-panel/active-containers/\n/docs/admin-panel/application-details/\n/docs/admin-panel/applications/\n/docs/admin-panel/common-screen-patterns/\n/docs/admin-panel/container-details/\n/docs/admin-panel/containers-history/\n/docs/admin-panel/dashboard/\n/docs/admin-panel/layout-and-navigation/\n/docs/admin-panel/profile/\n/docs/admin-panel/s3-secrets/\n/docs/admin-panel/tags/\n/docs/admin-panel/troubleshooting/\n/docs/admin-panel/users/\n/docs/admin-panel/warm-pools/\n/docs/api-v2/\n/docs/api-v2/artifacts/\n/docs/api-v2/authentication/\n/docs/api-v2/available-locations/\n/docs/api-v2/log-streaming/\n/docs/api-v2/open-api-spec/\n/docs/api-v2/reference/check-tag/\n/docs/api-v2/reference/create-application/\n/docs/api-v2/reference/create-token/\n/docs/api-v2/reference/delete-session/\n/docs/api-v2/reference/download-artifact/\n/docs/api-v2/reference/edit-application/\n/docs/api-v2/reference/enable-application-tag/\n/docs/api-v2/reference/get-locations/\n/docs/api-v2/reference/get-logs/\n/docs/api-v2/reference/get-session-id/\n/docs/api-v2/reference/get-sessions/\n/docs/api-v2/reference/list-application-tags-enabled/\n/docs/api-v2/reference/list-application-tags/\n/docs/api-v2/reference/list-applications/\n/docs/api-v2/reference/list-regions/\n/docs/api-v2/reference/post-session-player-join/\n/docs/api-v2/reference/post-session-player-leave/\n/docs/api-v2/reference/post-session/\n/docs/api-v2/session-management/\n/docs/api-v2/tag-verification/\n/docs/changelogs/\n/docs/concepts/\n/docs/faqs/\n/docs/faqs/api-authorisation/\n/docs/faqs/container-registries/\n/docs/faqs/how-gameye-scales/\n/docs/faqs/how-to-start/\n/docs/faqs/location-vs-region/\n/docs/faqs/long-lived-sessions/\n/docs/faqs/pricing/\n/docs/faqs/ttl-billing/\n/docs/faqs/windows-containers/\n/docs/favicon.svg\n/docs/getting-started/\n/docs/getting-started/1-docker-hub/\n/docs/getting-started/2-docker-file/\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8c861baee21828b9",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && rm -rf gspec && mkdir gspec && cd gspec && timeout 120 npm install @gameye/match-api-spec --no-audit --no-fund 2>&1 | tail -5; echo "=== files ==="; find node_modules/@gameye -maxdepth 2 -type f | head -20
npm error 404 '@oas3/oas3ts-lib@^4.0.0' is not in this registry.
npm error 404 '@oas3/oas3ts-lib@^4.0.0' is not in this registry. npm error 404 npm error 404 Note that you can also install from a npm error 404 tarball, folder, http url, or git url. npm error A complete log of this run can be found in: /sandbox/.npm/_logs/2026-10-16T14_54_34_262Z-debug-0.log === files === find: ‘node_modules/@gameye’: No such file or directory
Recorded tool call
{
"type": "tool_call",
"seq": 33,
"ts": "2026-10-16T14:54:34.435Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && rm -rf gspec && mkdir gspec && cd gspec && timeout 120 npm install @gameye/match-api-spec --no-audit --no-fund 2>&1 | tail -5; echo \"=== files ===\"; find node_modules/@gameye -maxdepth 2 -type f | head -20"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a70ad8e2f19e4e3d",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 35,
"ts": "2026-10-16T14:54:36.812Z",
"turn": 1,
"output": {
"content": [
{
"text": "npm error 404 '@oas3/oas3ts-lib@^4.0.0' is not in this registry.\nnpm error 404\nnpm error 404 Note that you can also install from a\nnpm error 404 tarball, folder, http url, or git url.\nnpm error A complete log of this run can be found in: /sandbox/.npm/_logs/2026-10-16T14_54_34_262Z-debug-0.log\n=== files ===\nfind: ‘node_modules/@gameye’: No such file or directory\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a70ad8e2f19e4e3d",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The @gameye/match-api-spec package can't install (broken transitive dep @oas3/oas3ts-lib not in registry). That's fine — I can fetch the OpenAPI spec directly from the docs page instead.
Let me fetch the key doc pages: api-v2/authentication, api-v2/open-api-spec, api-v2/session-management, api-v2/reference/post-session, and getting-started/4-api.
Let me get the raw OpenAPI spec. The open-api-spec page probably links to a yaml/json file.
Also fetch faqs/pricing and faqs/api-authorisation for pricing details and auth.
Let me do these in parallel.
The spec package has a broken dependency. Let me fetch the docs pages and the OpenAPI spec directly instead.
cd /tmp && for p in api-v2/authentication api-v2/open-api-spec api-v2/session-management getting-started/4-api faqs/api-authorisation; do f=$(echo $p|tr '/' '_'); timeout 30 curl -sSL "https://gameye.com/docs/$p/" -o "d_$f.html" -w "$p:%{http_code}\n"; doneapi-v2/authentication:200
api-v2/authentication:200 api-v2/open-api-spec:200 api-v2/session-management:200 getting-started/4-api:404 faqs/api-authorisation:200
Recorded tool call
{
"type": "tool_call",
"seq": 39,
"ts": "2026-10-16T14:54:39.293Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for p in api-v2/authentication api-v2/open-api-spec api-v2/session-management getting-started/4-api faqs/api-authorisation; do f=$(echo $p|tr '/' '_'); timeout 30 curl -sSL \"https://gameye.com/docs/$p/\" -o \"d_$f.html\" -w \"$p:%{http_code}\\n\"; done"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9f138334978ec8d4",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 42,
"ts": "2026-10-16T14:54:43.446Z",
"turn": 1,
"output": {
"content": [
{
"text": "api-v2/authentication:200\napi-v2/open-api-spec:200\napi-v2/session-management:200\ngetting-started/4-api:404\nfaqs/api-authorisation:200\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9f138334978ec8d4",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && python3 -c "
import re,html
for f in ['d_api-v2_authentication.html','d_getting-started_4-api.html']:
t=open(f,encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)
if m: t=m.group(0)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
print('='*25,f,'='*25); print(t.strip()[:2500])
"FailedTraceback (most recent call last):
Traceback (most recent call last): File "<string>", line 4, in <module> FileNotFoundError: [Errno 2] No such file or directory: 'd_api-v2_authentication.html' Command exited with code 1
Recorded tool call
{
"type": "tool_call",
"seq": 40,
"ts": "2026-10-16T14:54:39.293Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re,html\nfor f in ['d_api-v2_authentication.html','d_getting-started_4-api.html']:\n t=open(f,encoding='utf-8',errors='ignore').read()\n t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\n m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)\n if m: t=m.group(0)\n t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\n print('='*25,f,'='*25); print(t.strip()[:2500])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9952feb68d05ed94",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 41,
"ts": "2026-10-16T14:54:39.293Z",
"turn": 1,
"output": {
"content": [
{
"text": "Traceback (most recent call last):\n File \"<string>\", line 4, in <module>\nFileNotFoundError: [Errno 2] No such file or directory: 'd_api-v2_authentication.html'\n\n\nCommand exited with code 1",
"type": "text"
}
],
"details": {}
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9952feb68d05ed94",
"is_error": true,
"tool_kind": "shell"
}›Thinking…
Filename collision issue: api-v2/authentication -> d_api-v2_authentication.html. Wait, that should exist. Oh, the tr '/' '_' produced api-v2_authentication. Hmm, that's what I used. Let me list the files.
Actually wait, the error says file not found. Let me ls.
cd /tmp && ls -la d_*.html 2>/dev/null; echo "---"; ls *.html
-rw------- 1 root root 79425 Oct 16 14:54 d_api-v2_authentication.html
-rw------- 1 root root 79425 Oct 16 14:54 d_api-v2_authentication.html -rw------- 1 root root 61440 Oct 16 14:54 d_api-v2_open-api-spec.html -rw------- 1 root root 112891 Oct 16 14:54 d_api-v2_session-management.html -rw------- 1 root root 56224 Oct 16 14:54 d_faqs_api-authorisation.html -rw------- 1 root root 19705 Oct 16 14:54 d_getting-started_4-api.html --- d_api-v2_authentication.html d_api-v2_open-api-spec.html d_api-v2_session-management.html d_faqs_api-authorisation.html d_getting-started_4-api.html gdocs.html gprice.html
Recorded tool call
{
"type": "tool_call",
"seq": 45,
"ts": "2026-10-16T14:54:44.527Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && ls -la d_*.html 2>/dev/null; echo \"---\"; ls *.html"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-afc7176208164ce1",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 46,
"ts": "2026-10-16T14:54:44.527Z",
"turn": 1,
"output": {
"content": [
{
"text": "-rw------- 1 root root 79425 Oct 16 14:54 d_api-v2_authentication.html\n-rw------- 1 root root 61440 Oct 16 14:54 d_api-v2_open-api-spec.html\n-rw------- 1 root root 112891 Oct 16 14:54 d_api-v2_session-management.html\n-rw------- 1 root root 56224 Oct 16 14:54 d_faqs_api-authorisation.html\n-rw------- 1 root root 19705 Oct 16 14:54 d_getting-started_4-api.html\n---\nd_api-v2_authentication.html\nd_api-v2_open-api-spec.html\nd_api-v2_session-management.html\nd_faqs_api-authorisation.html\nd_getting-started_4-api.html\ngdocs.html\ngprice.html\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-afc7176208164ce1",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The files exist — the earlier python failed likely due to the second curl block running concurrently and... no, actually the issue was that the two tool calls ran in parallel, so the second one ran before the first finished. Files exist now. Let me read them.
Note getting-started/4-api was 404 (19705 bytes = 404 page). Let me find the right getting-started step 4 URL. From the docs nav: "4. Start with the API". Let me check the link list for getting-started paths.
Let me now extract the auth page and open-api-spec page.
Files were still downloading during the parallel call. Now let me read them.
cd /tmp && python3 -c "
import re,html
def txt(f,lim=3000):
t=open(f,encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)
if m: t=m.group(0)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
return t.strip()[:lim]
for f in ['d_api-v2_authentication.html','d_api-v2_open-api-spec.html']:
print('='*25,f,'='*25); print(txt(f))
"========================= d_api-v2_authentication.html =========================
========================= d_api-v2_authentication.html =========================
Authentication All endpoints in the Gameye Session API require authentication. This guide explains how to properly authenticate your requests.
Bearer Token Authentication Section titled “Bearer Token Authentication”
The Gameye Session API uses Bearer Token Authentication, which means you need to include an API token in the header of every request.
How to Use Your Token Section titled “How to Use Your Token”
Add your token to the Authorization header in this format:
Authorization: Bearer YOUR_TOKEN_HERE
Example Section titled “Example”
GET /session HTTP / 1.1 Host : api.production-gameye.gameye.net Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Obtaining a Token Section titled “Obtaining a Token”
Create or retrieve an API token in the Gameye Admin Panel :
Sign in to the Admin Panel.
Open Users and find the account that will own the token (use the search bar if needed).
Open the row Actions menu (⋮) and choose Edit .
On the Update User page, scroll to API Tokens :
New token: click + Add Token , set a name and scopes, then create the token. Copy the value immediately — the full token is only shown at creation time.
Existing token: click the eye icon next to the masked token to reveal it.
Store the token securely (environment variables or a secret manager).
For scope selection, disabling or deleting tokens, and security practices, see API tokens .
Creating Scoped Tokens Programmatically Section titled “Creating Scoped Tokens Programmatically”
You can create short-lived, scoped tokens via the API itself using POST /token . This is useful for delegation — for example, generating a token with only session:start permission to hand to a game client, or creating tokens that expire after a match ends.
cURL curl -X POST https://api.sandbox-gameye.gameye.net/token \ -H ' Authorization: Bearer YOUR_MASTER_TOKEN ' \ -H ' Content-Type: application/json ' \ -d ' { "name": "match-server-token", "scopes": ["session:start", "session:read"], "expires_after": "2h" } '
The response contains the new token string. Valid time units for expires_after are m and h (e.g. "30m" , "1h" , "2h30m" ); omit it for a token that never expires. A token can only delegate scopes it already holds — it cannot grant more permissions than the issuing token. The exception is webhook:github , which any token with token:write can grant.
See the POST /token reference for the full parameter list and error codes.
Token Security Section titled “Token Security”
Here are some best practices for keeping your token secure:
Never share your token in public repositories or client-side code
Use environment variables to store your token in your applications
Consider rotating your token periodically
Use the sandbox environment for testing
Authentication Errors Section titled “Authentication Errors”
If authentication fails, you’ll receive a 401 Unauthorized response with details about the error. Common reasons for authentication fail
========================= d_api-v2_open-api-spec.html =========================
Open API spec If you need these commands on hand, you can **download the Open API ** YAML file.
The Gameye Session API offers an agnostic way of hosting containerized game sessions without needing to worry about geographic capacity or scalability issues. The API gives you a suite of features to host your multiplayer game.
How do you authenticate? Section titled “How do you authenticate?”
All our endpoints need authorization. You can do this through Bearer Token Authentication.
You’ll need to ask our support team for your Bearer Token. Then, make sure you add it in the header of each request.
For example, if you wanted to use the endpoint to list current sessions, you’d write:
Terminal window $curl \ -X GET https://api.production-gameye.gameye.net/session \ -H " Authorization: Bearer $ACCESS_TOKEN "
Api token (HTTP) Section titled “Api token (HTTP)”
Send a bearer token (“token” Formatted) in the Authorization http header to authenticate with the API.
** Last updated: Oct 15, 2026 Previous Tag Verification Next POST /session
Recorded tool call
{
"type": "tool_call",
"seq": 50,
"ts": "2026-10-16T14:54:47.806Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re,html\ndef txt(f,lim=3000):\n t=open(f,encoding='utf-8',errors='ignore').read()\n t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\n m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)\n if m: t=m.group(0)\n t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\n return t.strip()[:lim]\nfor f in ['d_api-v2_authentication.html','d_api-v2_open-api-spec.html']:\n print('='*25,f,'='*25); print(txt(f))\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-88228d31e11bf79b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 53,
"ts": "2026-10-16T14:54:47.806Z",
"turn": 1,
"output": {
"content": [
{
"text": "========================= d_api-v2_authentication.html =========================\nAuthentication All endpoints in the Gameye Session API require authentication. This guide explains how to properly authenticate your requests. \n Bearer Token Authentication Section titled “Bearer Token Authentication” \n The Gameye Session API uses Bearer Token Authentication, which means you need to include an API token in the header of every request. \n How to Use Your Token Section titled “How to Use Your Token” \n Add your token to the Authorization header in this format: \n Authorization: Bearer YOUR_TOKEN_HERE \n Example Section titled “Example” \n GET /session HTTP / 1.1 Host : api.production-gameye.gameye.net Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \n Obtaining a Token Section titled “Obtaining a Token” \n Create or retrieve an API token in the Gameye Admin Panel : \n Sign in to the Admin Panel. \n Open Users and find the account that will own the token (use the search bar if needed). \n Open the row Actions menu (⋮) and choose Edit . \n On the Update User page, scroll to API Tokens :\n New token: click + Add Token , set a name and scopes, then create the token. Copy the value immediately — the full token is only shown at creation time. \n Existing token: click the eye icon next to the masked token to reveal it. \n Store the token securely (environment variables or a secret manager). \n For scope selection, disabling or deleting tokens, and security practices, see API tokens . \n Creating Scoped Tokens Programmatically Section titled “Creating Scoped Tokens Programmatically” \n You can create short-lived, scoped tokens via the API itself using POST /token . This is useful for delegation — for example, generating a token with only session:start permission to hand to a game client, or creating tokens that expire after a match ends. \n cURL curl -X POST https://api.sandbox-gameye.gameye.net/token \\ -H ' Authorization: Bearer YOUR_MASTER_TOKEN ' \\ -H ' Content-Type: application/json ' \\ -d ' { \"name\": \"match-server-token\", \"scopes\": [\"session:start\", \"session:read\"], \"expires_after\": \"2h\" } ' \n The response contains the new token string. Valid time units for expires_after are m and h (e.g. \"30m\" , \"1h\" , \"2h30m\" ); omit it for a token that never expires. A token can only delegate scopes it already holds — it cannot grant more permissions than the issuing token. The exception is webhook:github , which any token with token:write can grant. \n See the POST /token reference for the full parameter list and error codes. \n Token Security Section titled “Token Security” \n Here are some best practices for keeping your token secure: \n Never share your token in public repositories or client-side code \n Use environment variables to store your token in your applications \n Consider rotating your token periodically \n Use the sandbox environment for testing \n Authentication Errors Section titled “Authentication Errors” \n If authentication fails, you’ll receive a 401 Unauthorized response with details about the error. Common reasons for authentication fail\n========================= d_api-v2_open-api-spec.html =========================\nOpen API spec If you need these commands on hand, you can **download the Open API ** YAML file. \n The Gameye Session API offers an agnostic way of hosting containerized game sessions without needing to worry about geographic capacity or scalability issues. The API gives you a suite of features to host your multiplayer game. \n How do you authenticate? Section titled “How do you authenticate?” \n All our endpoints need authorization. You can do this through Bearer Token Authentication. \n You’ll need to ask our support team for your Bearer Token. Then, make sure you add it in the header of each request. \n For example, if you wanted to use the endpoint to list current sessions, you’d write: \n Terminal window $curl \\ -X GET https://api.production-gameye.gameye.net/session \\ -H \" Authorization: Bearer $ACCESS_TOKEN \" \n Api token (HTTP) Section titled “Api token (HTTP)” \n Send a bearer token (“token” Formatted) in the Authorization http header to authenticate with the API. \n ** Last updated: Oct 15, 2026 Previous Tag Verification Next POST /session\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-88228d31e11bf79b",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && python3 -c "
import re
t=open('gdocs.html',encoding='utf-8',errors='ignore').read()
for l in sorted(set(re.findall(r'href=\"(/docs/getting-started[^\"]*)\"',t))): print(l)
"/docs/getting-started/
/docs/getting-started/ /docs/getting-started/1-docker-hub/ /docs/getting-started/2-docker-file/ /docs/getting-started/3-admin-panel/ /docs/getting-started/4-api-call/ /docs/getting-started/ghcr/
Recorded tool call
{
"type": "tool_call",
"seq": 51,
"ts": "2026-10-16T14:54:47.806Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gdocs.html',encoding='utf-8',errors='ignore').read()\nfor l in sorted(set(re.findall(r'href=\\\"(/docs/getting-started[^\\\"]*)\\\"',t))): print(l)\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-912ea7e50e5973e2",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 52,
"ts": "2026-10-16T14:54:47.806Z",
"turn": 1,
"output": {
"content": [
{
"text": "/docs/getting-started/\n/docs/getting-started/1-docker-hub/\n/docs/getting-started/2-docker-file/\n/docs/getting-started/3-admin-panel/\n/docs/getting-started/4-api-call/\n/docs/getting-started/ghcr/\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-912ea7e50e5973e2",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Now let me get the real OpenAPI spec YAML and the getting-started/4-api-call page and session-management page. The open-api-spec page links to a downloadable YAML — let me find the link.
Then I'll build the example. Key: there's a sandbox environment api.sandbox-gameye.gameye.net and production api.production-gameye.gameye.net.
I need a token to actually run. No token in env. So I will:
- Download the real OpenAPI spec (source of truth).
- Write a runnable example client (Node/TS or plain JS) that does: list regions → available locations → start session → poll → player join → logs → stop. With token from env
GAMEYE_API_TOKEN. - Verify connectivity + confirm the exact blocker (401 without token).
- Validate my example against the spec programmatically so paths/params aren't hallucinated.
Let me get the spec link and the 4-api-call page.
Let me get the OpenAPI spec and the "Start with the API" quickstart page.
cd /tmp && grep -oE 'href="[^"]*\.(yaml|yml|json)[^"]*"' d_api-v2_open-api-spec.html | sort -u; echo "--- any gameye.net/openapi links ---"; grep -oE 'https?://[^"'"'"' ]*(openapi|open-api|swagger)[^"'"'"' ]*' d_api-v2_open-api-spec.html | sort -u | head
href="https://api.production-gameye.gameye.net/openapi.yaml"
href="https://api.production-gameye.gameye.net/openapi.yaml" --- any gameye.net/openapi links --- https://api.production-gameye.gameye.net/openapi.yaml https://gameye.com/docs/api-v2/open-api-spec/
Recorded tool call
{
"type": "tool_call",
"seq": 57,
"ts": "2026-10-16T14:54:51.170Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && grep -oE 'href=\"[^\"]*\\.(yaml|yml|json)[^\"]*\"' d_api-v2_open-api-spec.html | sort -u; echo \"--- any gameye.net/openapi links ---\"; grep -oE 'https?://[^\"'\"'\"' ]*(openapi|open-api|swagger)[^\"'\"'\"' ]*' d_api-v2_open-api-spec.html | sort -u | head"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a5d6bec266ffe1a9",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 59,
"ts": "2026-10-16T14:54:51.170Z",
"turn": 1,
"output": {
"content": [
{
"text": "href=\"https://api.production-gameye.gameye.net/openapi.yaml\"\n--- any gameye.net/openapi links ---\nhttps://api.production-gameye.gameye.net/openapi.yaml\nhttps://gameye.com/docs/api-v2/open-api-spec/\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a5d6bec266ffe1a9",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && timeout 30 curl -sSL "https://gameye.com/docs/getting-started/4-api-call/" -o d_4api.html -w "%{http_code}\n" && python3 -c "
import re,html
t=open('d_4api.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)
if m: t=m.group(0)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
print(t.strip()[:3500])
"200
200
4. Start with the API In production, your backend or matchmaker starts every session through the Gameye API. This page takes you from an API token to a running session and back.
You need an application with at least one tag loaded on nodes. Create it in the Admin Panel or with the create application endpoint.
For every endpoint and field, see the API V2 overview and the OpenAPI specification .
1. Get an API token Section titled “1. Get an API token”
Create a token in the Admin Panel under Users → API tokens , or with the create token endpoint. Give it the scopes this guide uses:
Scope Used for regions:read Finding the regions where your image can run session:start Starting sessions session:read Listing and inspecting sessions session:stop Stopping sessions
Send it as a bearer token. The examples below assume it is in $GAMEYE_TOKEN :
Terminal window export GAMEYE_TOKEN = " <your API token> " "">
2. Find where your image can run Section titled “2. Find where your image can run”
A session’s location is a region name, such as eu-west-1 . Ask which regions can run your application right now, using its application name:
Terminal window curl https://api.production-gameye.gameye.net/available-location/my-game \ -H " Authorization: Bearer $GAMEYE_TOKEN "
The response lists each region’s id and the IP addresses clients can ping to measure latency. See Get locations .
3. Start a session Section titled “3. Start a session”
Terminal window curl -X POST https://api.production-gameye.gameye.net/session \ -H " Authorization: Bearer $GAMEYE_TOKEN " \ -H " Content-Type: application/json " \ -d ' { "id": "f7965a22-e327-4541-a30e-8262890d3a29", "image": "my-game", "location": "eu-west-1", "ttl": "1h", "args": ["--max-players=8"], "env": { "MATCH_MODE": "ranked" }, "labels": { "match": "1234" } } '
Only image (your application name) and location are required:
Field Meaning id Your identifier for the session. Use a UUID. If you leave it out, Gameye generates one. version The image tag to run. If you leave it out, Gameye runs the newest loaded tag. ttl How long the session may live before Gameye stops it, in minutes or hours ( "30m" , "2h" ). args , env , labels Command-line arguments, environment variables and labels for this session. restart Restart the container automatically if it crashes.
A successful call returns 201 Created with the address players connect to:
{ "id" : " f7965a22-e327-4541-a30e-8262890d3a29 " , "host" : " 203.0.113.42 " , "ports" : [ { "type" : " udp " , "container" : 7777 , "host" : 49152 } ] }
Players connect to host on the matching ports[].host port, here 203.0.113.42:49152 . Inside the container, the same port is available as GAMEYE_PORT_UDP_7777 .
4. Check and stop sessions Section titled “4. Check and stop sessions”
List your running sessions with GET /session , or inspect one with GET /session/{id} . See Get sessions .
Stop a session when its match ends:
Terminal window curl -X DELETE https://api.production-gameye.gameye.net/session/f7965a22-e327-4541-a30e-8262890d3a29 \ -H " Authorization: Bearer $GAMEYE_TOKEN "
A successful stop returns 204 No Content . A session you don’t stop keeps running until its ttl expires, so always set a ttl as a safety net.
Troubleshooting Section titled “Troubleshooting”
Response or symptom What to check 401 Unauthorized The token is missing, mistyped, or not sent as Authorization: Bearer <token> . 402 Payment Required Your organization has used up i
Recorded tool call
{
"type": "tool_call",
"seq": 58,
"ts": "2026-10-16T14:54:51.170Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 30 curl -sSL \"https://gameye.com/docs/getting-started/4-api-call/\" -o d_4api.html -w \"%{http_code}\\n\" && python3 -c \"\nimport re,html\nt=open('d_4api.html',encoding='utf-8',errors='ignore').read()\nt=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\nm=re.search(r'<main.*?</main>',t,flags=re.S|re.I)\nif m: t=m.group(0)\nt=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\nprint(t.strip()[:3500])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a8b809e0f95aa4f4",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 60,
"ts": "2026-10-16T14:54:51.816Z",
"turn": 1,
"output": {
"content": [
{
"text": "200\n4. Start with the API In production, your backend or matchmaker starts every session through the Gameye API. This page takes you from an API token to a running session and back. \n You need an application with at least one tag loaded on nodes. Create it in the Admin Panel or with the create application endpoint. \n For every endpoint and field, see the API V2 overview and the OpenAPI specification . \n 1. Get an API token Section titled “1. Get an API token” \n Create a token in the Admin Panel under Users → API tokens , or with the create token endpoint. Give it the scopes this guide uses: \n Scope Used for regions:read Finding the regions where your image can run session:start Starting sessions session:read Listing and inspecting sessions session:stop Stopping sessions \n Send it as a bearer token. The examples below assume it is in $GAMEYE_TOKEN : \n Terminal window export GAMEYE_TOKEN = \" <your API token> \" \"\"> \n 2. Find where your image can run Section titled “2. Find where your image can run” \n A session’s location is a region name, such as eu-west-1 . Ask which regions can run your application right now, using its application name: \n Terminal window curl https://api.production-gameye.gameye.net/available-location/my-game \\ -H \" Authorization: Bearer $GAMEYE_TOKEN \" \n The response lists each region’s id and the IP addresses clients can ping to measure latency. See Get locations . \n 3. Start a session Section titled “3. Start a session” \n Terminal window curl -X POST https://api.production-gameye.gameye.net/session \\ -H \" Authorization: Bearer $GAMEYE_TOKEN \" \\ -H \" Content-Type: application/json \" \\ -d ' { \"id\": \"f7965a22-e327-4541-a30e-8262890d3a29\", \"image\": \"my-game\", \"location\": \"eu-west-1\", \"ttl\": \"1h\", \"args\": [\"--max-players=8\"], \"env\": { \"MATCH_MODE\": \"ranked\" }, \"labels\": { \"match\": \"1234\" } } ' \n Only image (your application name) and location are required: \n Field Meaning id Your identifier for the session. Use a UUID. If you leave it out, Gameye generates one. version The image tag to run. If you leave it out, Gameye runs the newest loaded tag. ttl How long the session may live before Gameye stops it, in minutes or hours ( \"30m\" , \"2h\" ). args , env , labels Command-line arguments, environment variables and labels for this session. restart Restart the container automatically if it crashes. \n A successful call returns 201 Created with the address players connect to: \n { \"id\" : \" f7965a22-e327-4541-a30e-8262890d3a29 \" , \"host\" : \" 203.0.113.42 \" , \"ports\" : [ { \"type\" : \" udp \" , \"container\" : 7777 , \"host\" : 49152 } ] } \n Players connect to host on the matching ports[].host port, here 203.0.113.42:49152 . Inside the container, the same port is available as GAMEYE_PORT_UDP_7777 . \n 4. Check and stop sessions Section titled “4. Check and stop sessions” \n List your running sessions with GET /session , or inspect one with GET /session/{id} . See Get sessions . \n Stop a session when its match ends: \n Terminal window curl -X DELETE https://api.production-gameye.gameye.net/session/f7965a22-e327-4541-a30e-8262890d3a29 \\ -H \" Authorization: Bearer $GAMEYE_TOKEN \" \n A successful stop returns 204 No Content . A session you don’t stop keeps running until its ttl expires, so always set a ttl as a safety net. \n Troubleshooting Section titled “Troubleshooting” \n Response or symptom What to check 401 Unauthorized The token is missing, mistyped, or not sent as Authorization: Bearer <token> . 402 Payment Required Your organization has used up i\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a8b809e0f95aa4f4",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Excellent — this is a real, actionable API workflow. There's a public OpenAPI spec at https://api.production-gameye.gameye.net/openapi.yaml. Let me download it and use it as source of truth.
Then build the example. I need to determine: can I do anything without a token? I'll verify connectivity and confirm the 401 blocker.
Note also the sandbox host: api.sandbox-gameye.gameye.net.
Let me download the spec.
The OpenAPI spec is public. Let me download it as the source of truth and probe the API's reachability.
cd /tmp && timeout 30 curl -sSL "https://api.production-gameye.gameye.net/openapi.yaml" -o gameye-openapi.yaml -w "spec:%{http_code}\n"; wc -l gameye-openapi.yaml; head -30 gameye-openapi.yamlspec:200
spec:200
1824 gameye-openapi.yaml
openapi: 3.0.0
info:
title: Gameye Session API
version: 1.2.1
description: >
The Gameye Session API offers an agnostic way of hosting containerized game
sessions without having to worry about geographic capacity or scalability issues.
The API offers a complete suite of features to host a server based multiplayer game.
# Authentication
All endpoints of the Gameye Session API require authorization. This is done
through Bearer Token Authentication. A bearer token is thus required in the
HTTP header of each request.
For example:
```Authorization: Bearer <token>```
x-logo:
url: https://static.gameye.com/images/android-chrome-512x512.png
altText: Gameye Logo
servers:
- url: https://api.sandbox-gameye.gameye.net
description: The main sandbox environment.
- url: https://api.production-gameye.gameye.net
description: The main production environment.
paths:
/session:
Recorded tool call
{
"type": "tool_call",
"seq": 64,
"ts": "2026-10-16T14:54:54.645Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 30 curl -sSL \"https://api.production-gameye.gameye.net/openapi.yaml\" -o gameye-openapi.yaml -w \"spec:%{http_code}\\n\"; wc -l gameye-openapi.yaml; head -30 gameye-openapi.yaml"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9c0b576732be604c",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 66,
"ts": "2026-10-16T14:54:56.229Z",
"turn": 1,
"output": {
"content": [
{
"text": "spec:200\n1824 gameye-openapi.yaml\nopenapi: 3.0.0\ninfo:\n title: Gameye Session API\n version: 1.2.1\n description: >\n The Gameye Session API offers an agnostic way of hosting containerized game\n sessions without having to worry about geographic capacity or scalability issues. \n The API offers a complete suite of features to host a server based multiplayer game.\n\n # Authentication\n\n All endpoints of the Gameye Session API require authorization. This is done\n through Bearer Token Authentication. A bearer token is thus required in the \n HTTP header of each request.\n\n For example:\n ```Authorization: Bearer <token>```\n\n x-logo:\n url: https://static.gameye.com/images/android-chrome-512x512.png\n altText: Gameye Logo\n\nservers:\n - url: https://api.sandbox-gameye.gameye.net\n description: The main sandbox environment.\n - url: https://api.production-gameye.gameye.net\n description: The main production environment.\n\npaths:\n /session:\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9c0b576732be604c",
"is_error": false,
"tool_kind": "shell"
}echo "=== no-token probe (sandbox) ==="; timeout 20 curl -sS -o /tmp/p1.txt -w "sandbox /session -> %{http_code}\n" https://api.sandbox-gameye.gameye.net/session; head -c 300 /tmp/p1.txt; echo; echo "=== no-token probe (production) ==="; timeout 20 curl -sS -o /tmp/p2.txt -w "prod /session -> %{http_code}\n" https://api.production-gameye.gameye.net/session; head -c 300 /tmp/p2.txt; echo; echo "=== bogus-token probe ==="; timeout 20 curl -sS -o /tmp/p3.txt -w "prod w/ bogus bearer -> %{http_code}\n" -H "Authorization: Bearer not-a-real-token" https://api.production-gameye.gameye.net/session; head -c 300 /tmp/p3.txt=== no-token probe (sandbox) ===
=== no-token probe (sandbox) ===
sandbox /session -> 401
{"code":"UNAUTHORIZED","details":"The provided token is malformed, please ensure you provide a token of the form 'Bearer \u003ctoken\u003e'","identifier":"6019f1cb-9634-4054-832b-86529db4415f","message":"Unable to authorize request.","path":"/session","statusCode":401,"timestamp":"2026-10-16T14:54:5
=== no-token probe (production) ===
prod /session -> 401
{"code":"UNAUTHORIZED","details":"The provided token is malformed, please ensure you provide a token of the form 'Bearer \u003ctoken\u003e'","identifier":"9565c118-d564-4c60-8617-af8503780182","message":"Unable to authorize request.","path":"/session","statusCode":401,"timestamp":"2026-10-16T14:54:5
=== bogus-token probe ===
prod w/ bogus bearer -> 401
{"code":"UNAUTHORIZED","details":"Please verify that you are using a valid auth token.","identifier":"fe919218-47be-4808-adf7-a4f5122235fc","message":"Unable to authorize request.","path":"/session","statusCode":401,"timestamp":"2026-10-16T14:54:56Z","violations":[]}
Recorded tool call
{
"type": "tool_call",
"seq": 65,
"ts": "2026-10-16T14:54:54.645Z",
"name": "bash",
"turn": 1,
"input": {
"command": "echo \"=== no-token probe (sandbox) ===\"; timeout 20 curl -sS -o /tmp/p1.txt -w \"sandbox /session -> %{http_code}\\n\" https://api.sandbox-gameye.gameye.net/session; head -c 300 /tmp/p1.txt; echo; echo \"=== no-token probe (production) ===\"; timeout 20 curl -sS -o /tmp/p2.txt -w \"prod /session -> %{http_code}\\n\" https://api.production-gameye.gameye.net/session; head -c 300 /tmp/p2.txt; echo; echo \"=== bogus-token probe ===\"; timeout 20 curl -sS -o /tmp/p3.txt -w \"prod w/ bogus bearer -> %{http_code}\\n\" -H \"Authorization: Bearer not-a-real-token\" https://api.production-gameye.gameye.net/session; head -c 300 /tmp/p3.txt"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bb3afd92cfd6cb59",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 67,
"ts": "2026-10-16T14:54:58.171Z",
"turn": 1,
"output": {
"content": [
{
"text": "=== no-token probe (sandbox) ===\nsandbox /session -> 401\n{\"code\":\"UNAUTHORIZED\",\"details\":\"The provided token is malformed, please ensure you provide a token of the form 'Bearer \\u003ctoken\\u003e'\",\"identifier\":\"6019f1cb-9634-4054-832b-86529db4415f\",\"message\":\"Unable to authorize request.\",\"path\":\"/session\",\"statusCode\":401,\"timestamp\":\"2026-10-16T14:54:5\n=== no-token probe (production) ===\nprod /session -> 401\n{\"code\":\"UNAUTHORIZED\",\"details\":\"The provided token is malformed, please ensure you provide a token of the form 'Bearer \\u003ctoken\\u003e'\",\"identifier\":\"9565c118-d564-4c60-8617-af8503780182\",\"message\":\"Unable to authorize request.\",\"path\":\"/session\",\"statusCode\":401,\"timestamp\":\"2026-10-16T14:54:5\n=== bogus-token probe ===\nprod w/ bogus bearer -> 401\n{\"code\":\"UNAUTHORIZED\",\"details\":\"Please verify that you are using a valid auth token.\",\"identifier\":\"fe919218-47be-4808-adf7-a4f5122235fc\",\"message\":\"Unable to authorize request.\",\"path\":\"/session\",\"statusCode\":401,\"timestamp\":\"2026-10-16T14:54:56Z\",\"violations\":[]}\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bb3afd92cfd6cb59",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The API is live and reachable. The exact blocker: I need a valid API token, which requires a Gameye Admin Panel account. No credentials in env.
Now, what I CAN do: build a real, runnable example validated against the live OpenAPI spec. That's the valuable deliverable. Let me:
- Extract the exact endpoints/schemas from the spec (post-session request/response, get-sessions, player join/leave, logs, regions, available-location, delete session).
- Write a small Node.js example (no build step, plain JS with fetch — Node 22 has global fetch, zero dependencies) that runs the full lifecycle.
- Add a spec-validation script that checks every path+method I use exists in the downloaded spec — so nothing is hallucinated.
- Run it without a token to demonstrate it correctly reports the blocker.
Let me inspect the spec for the exact schemas I need.
API is live; the only blocker is a token. Let me extract exact schemas from the spec so nothing is guessed.
cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
# list all paths and methods
cur=None
for line in t.split('\n'):
m=re.match(r'^ (/[^:]*):$',line)
if m: cur=m.group(1); print('PATH',cur); continue
m=re.match(r'^ (get|post|put|delete|patch):$',line)
if m and cur: print(' ',m.group(1).upper())
"PATH /session
PATH /session
POST
GET
PATH /session/{id}
GET
DELETE
PATH /session/player/join
PUT
PATH /session/player/leave
DELETE
PATH /artifacts
GET
PATH /token
POST
PATH /application
POST
GET
PATH /application/{name}/tags
POST
PATH /application/{name}/tags/available
GET
PATH /application/{name}
PUT
PATH /region
GET
PATH /available-location/{image}
GET
PATH /tag/{region}/{image}/{version}
GET
PATH /logs
GET
Recorded tool call
{
"type": "tool_call",
"seq": 71,
"ts": "2026-10-16T14:54:59.546Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\n# list all paths and methods\ncur=None\nfor line in t.split('\\n'):\n m=re.match(r'^ (/[^:]*):$',line)\n if m: cur=m.group(1); print('PATH',cur); continue\n m=re.match(r'^ (get|post|put|delete|patch):$',line)\n if m and cur: print(' ',m.group(1).upper())\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a01c95da88270d03",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 74,
"ts": "2026-10-16T14:54:59.546Z",
"turn": 1,
"output": {
"content": [
{
"text": "PATH /session\n POST\n GET\nPATH /session/{id}\n GET\n DELETE\nPATH /session/player/join\n PUT\nPATH /session/player/leave\n DELETE\nPATH /artifacts\n GET\nPATH /token\n POST\nPATH /application\n POST\n GET\nPATH /application/{name}/tags\n POST\nPATH /application/{name}/tags/available\n GET\nPATH /application/{name}\n PUT\nPATH /region\n GET\nPATH /available-location/{image}\n GET\nPATH /tag/{region}/{image}/{version}\n GET\nPATH /logs\n GET\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a01c95da88270d03",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
i=t.index(' /session:')
print(t[i:i+3000])
"/session:
/session:
post:
operationId: session-run
description: >
Allocate a new game session in a specific region. Each session is to be
identified by a unique identifier that is provided by the caller. It is
recommended to use a UUID.
In case a warm pool is configured for the specified region and image, an
already spun up session will be taken from the pool instead and the given
set of program arguments, environment variables and additional labels in
the HTTP request will be ignored. Sessions managed by a warm pool will
always use the same static set of program arguments and environment variables.
The warm pool will then as soon as possible try to replenish itself. If the
warm pool is exhausted, a cold allocation will occur as normal.
security:
- api-token: ['session:start']
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SessionRunBody'
responses:
201:
description: >
A session is running on a machine in the provided region.
content:
application/json:
schema:
$ref: '#/components/schemas/SessionRunOkBody'
401:
description: >
You are not authorized to run a session. Please make sure that the
included bearer token is correct.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
402:
description: >
You have exhausted your quota.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
403:
description: >
Your API token may not have the required permissions to invoke this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
404:
description: >
The request contains a region or application that does not exist.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
409:
description: >
Cannot start a session with the provided id. Please use a unique
identifier for your session. It is recommended to use a UUID.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
420:
description: >
This session can currently not be started in the provided region.
This could also be the result of starting too many sessions in a short period of time.
Please try again later!
content:
application/json:
schema:
Recorded tool call
{
"type": "tool_call",
"seq": 72,
"ts": "2026-10-16T14:54:59.546Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\ni=t.index(' /session:')\nprint(t[i:i+3000])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bdc7b58e603e80a6",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 73,
"ts": "2026-10-16T14:54:59.546Z",
"turn": 1,
"output": {
"content": [
{
"text": " /session:\n post:\n operationId: session-run\n description: >\n Allocate a new game session in a specific region. Each session is to be\n identified by a unique identifier that is provided by the caller. It is \n recommended to use a UUID.\n\n In case a warm pool is configured for the specified region and image, an\n already spun up session will be taken from the pool instead and the given\n set of program arguments, environment variables and additional labels in\n the HTTP request will be ignored. Sessions managed by a warm pool will\n always use the same static set of program arguments and environment variables.\n The warm pool will then as soon as possible try to replenish itself. If the\n warm pool is exhausted, a cold allocation will occur as normal.\n security:\n - api-token: ['session:start']\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/SessionRunBody'\n\n responses:\n 201:\n description: >\n A session is running on a machine in the provided region.\n\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/SessionRunOkBody'\n\n 401:\n description: >\n You are not authorized to run a session. Please make sure that the\n included bearer token is correct.\n\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n\n 402:\n description: >\n You have exhausted your quota.\n\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n\n 403:\n description: >\n Your API token may not have the required permissions to invoke this endpoint.\n\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n\n 404:\n description: >\n The request contains a region or application that does not exist.\n\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n\n 409:\n description: >\n Cannot start a session with the provided id. Please use a unique\n identifier for your session. It is recommended to use a UUID.\n\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 420:\n description: >\n This session can currently not be started in the provided region.\n This could also be the result of starting too many sessions in a short period of time. \n Please try again later!\n\n content:\n application/json:\n schema:\n \n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bdc7b58e603e80a6",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Now let me get the key schemas: SessionRunBody, SessionRunOkBody, session list response, player join/leave bodies, logs params.
cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
for name in ['SessionRunBody','SessionRunOkBody','SessionOkBody','Port']:
m=re.search(r'^ '+name+r':\n(.*?)(?=^ \w)',t,flags=re.S|re.M)
if m: print('###',name); print(m.group(0)[:1600])
"### SessionRunBody
### SessionRunBody
SessionRunBody:
type: object
additionalProperties: false
required:
- location
- image
properties:
id:
type: string
description: |
The unique identifier of the session. If undefined, Gameye will generate a UUIDV4
for you and return it in the API response.
minLength: 1
example: f7965a22-e327-4541-a30e-8262890d3a29
location:
type: string
description: >
The region the game session is to be hosted in.
minLength: 1
example: europe
image:
type: string
description: >
The name of the application.
minLength: 1
example: nginx
env:
type: object
description: >
The list of environment variables to feed the underlying container on startup.
This field is ignored when a container is claimed from a warm pool.
additionalProperties:
type: string
minLength: 1
example:
env:
my-secret: <some_very_secret_value>
args:
type: array
description: >
The list of program console arguments to feed to the application hosted inside
the underlying container. This field is ignored when a container is claimed
from a warm pool.
items:
type: string
minLength: 1
example:
- '--max-players=8'
- '--match-type=quick-play'
resta
### SessionRunOkBody
SessionRunOkBody:
type: object
additionalProperties: false
required:
- host
- ports
properties:
id:
type: string
description: |
The generated unique session ID.
host:
type: string
description: |
The IP address of the machine that the session is running on.
ports:
type: array
description: |
The list of mapped ports for this session.
items:
$ref: '#/components/schemas/Port'
example:
id: c5c7c507-f5de-443e-b772-af4b444cfc21
host: a.b.c.d
ports:
- type: tcp
container: 80
host: 49159
- type: tcp
container: 80
host: 49159
### Port
Port:
type: object
additionalProperties: false
required:
- type
- container
- host
properties:
type:
type: string
description: |
The type of protocol of this port entry.
enum:
- tcp
- udp
container:
type: integer
description: >
The port of the container that is hosted on the
machine.
minimum: 1
maximum: 65535
host:
type: integer
description: |
The host port for mapping the container port.
minimum: 1
maximum: 65535
Recorded tool call
{
"type": "tool_call",
"seq": 77,
"ts": "2026-10-16T14:55:02.528Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\nfor name in ['SessionRunBody','SessionRunOkBody','SessionOkBody','Port']:\n m=re.search(r'^ '+name+r':\\n(.*?)(?=^ \\w)',t,flags=re.S|re.M)\n if m: print('###',name); print(m.group(0)[:1600])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-814dd25976ffaeca",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 80,
"ts": "2026-10-16T14:55:02.528Z",
"turn": 1,
"output": {
"content": [
{
"text": "### SessionRunBody\n SessionRunBody:\n type: object\n additionalProperties: false\n required:\n - location\n - image\n properties:\n id:\n type: string\n description: |\n The unique identifier of the session. If undefined, Gameye will generate a UUIDV4\n for you and return it in the API response.\n minLength: 1\n example: f7965a22-e327-4541-a30e-8262890d3a29\n location:\n type: string\n description: >\n The region the game session is to be hosted in.\n minLength: 1\n example: europe\n image:\n type: string\n description: >\n The name of the application.\n minLength: 1\n example: nginx\n env:\n type: object\n description: >\n The list of environment variables to feed the underlying container on startup.\n This field is ignored when a container is claimed from a warm pool.\n additionalProperties:\n type: string\n minLength: 1\n example:\n env:\n my-secret: <some_very_secret_value>\n args:\n type: array\n description: >\n The list of program console arguments to feed to the application hosted inside \n the underlying container. This field is ignored when a container is claimed \n from a warm pool.\n items:\n type: string\n minLength: 1\n example:\n - '--max-players=8'\n - '--match-type=quick-play'\n resta\n### SessionRunOkBody\n SessionRunOkBody:\n type: object\n additionalProperties: false\n required:\n - host\n - ports\n properties:\n id:\n type: string\n description: |\n The generated unique session ID.\n host:\n type: string\n description: |\n The IP address of the machine that the session is running on.\n ports:\n type: array\n description: |\n The list of mapped ports for this session.\n items:\n $ref: '#/components/schemas/Port'\n example:\n id: c5c7c507-f5de-443e-b772-af4b444cfc21\n host: a.b.c.d\n ports:\n - type: tcp\n container: 80\n host: 49159\n - type: tcp\n container: 80\n host: 49159\n\n\n### Port\n Port:\n type: object\n additionalProperties: false\n required:\n - type\n - container\n - host\n properties:\n type:\n type: string\n description: |\n The type of protocol of this port entry.\n enum:\n - tcp\n - udp\n container:\n type: integer\n description: >\n The port of the container that is hosted on the\n machine.\n minimum: 1\n maximum: 65535\n host:\n type: integer\n description: |\n The host port for mapping the container port.\n minimum: 1\n maximum: 65535\n\n\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-814dd25976ffaeca",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
i=t.index(' get:\n operationId: session-list') if 'session-list' in t else t.index('/session/{id}:')
print(t[i:i+1200])
print('===== PLAYER JOIN =====')
j=t.index('/session/player/join:')
print(t[j:j+1500])
"get:
get:
operationId: session-list
parameters:
- name: all
in: query
required: false
schema:
type: boolean
description: |
Whether to return all sessions regardless of status in the response.
The default value for this is 'false', which would only return running sessions.
- name: location
in: query
required: false
schema:
type: string
description: |
Filter results by location.
- name: image
in: query
required: false
schema:
type: string
description: |
Filter results by image.
- name: tag
in: query
required: false
schema:
type: string
description: |
Filter results by tag.
- name: host
in: query
required: false
schema:
type: string
description: |
Filter results by host IP address.
- name: playerCount
in: query
required: false
style: deepObject
schema:
===== PLAYER JOIN =====
/session/player/join:
put:
operationId: join-session
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/JoinSession'
responses:
200:
description: >
One or more player IDs have been registered as having
joined this session.
content:
application/json:
schema:
$ref: '#/components/schemas/JoinSessionOkBody'
401:
description: >
You are not authorized to start a session. Please make sure that the
included bearer token is correct.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
403:
description: >
Your API token may not have the required permissions to invoke this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
404:
description: >
The provided session does not exist.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
422:
description: >
Bad request. This may imply that the given request has invalid syntax.
content:
application/json:
Recorded tool call
{
"type": "tool_call",
"seq": 78,
"ts": "2026-10-16T14:55:02.528Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\ni=t.index(' get:\\n operationId: session-list') if 'session-list' in t else t.index('/session/{id}:')\nprint(t[i:i+1200])\nprint('===== PLAYER JOIN =====')\nj=t.index('/session/player/join:')\nprint(t[j:j+1500])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-99cdb5e8b9520368",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 79,
"ts": "2026-10-16T14:55:02.528Z",
"turn": 1,
"output": {
"content": [
{
"text": " get:\n operationId: session-list\n parameters:\n - name: all\n in: query\n required: false\n schema:\n type: boolean\n description: |\n Whether to return all sessions regardless of status in the response. \n The default value for this is 'false', which would only return running sessions.\n\n - name: location\n in: query\n required: false\n schema:\n type: string\n description: |\n Filter results by location.\n\n - name: image\n in: query\n required: false\n schema:\n type: string\n description: |\n Filter results by image.\n\n - name: tag\n in: query\n required: false\n schema:\n type: string\n description: |\n Filter results by tag.\n\n - name: host\n in: query\n required: false\n schema:\n type: string\n description: |\n Filter results by host IP address.\n\n - name: playerCount\n in: query\n required: false\n style: deepObject\n schema:\n \n===== PLAYER JOIN =====\n/session/player/join:\n put:\n operationId: join-session\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/JoinSession'\n\n responses:\n 200:\n description: >\n One or more player IDs have been registered as having\n joined this session.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/JoinSessionOkBody'\n\n 401:\n description: >\n You are not authorized to start a session. Please make sure that the\n included bearer token is correct.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 403:\n description: >\n Your API token may not have the required permissions to invoke this endpoint.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 404:\n description: >\n The provided session does not exist.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 422:\n description: >\n Bad request. This may imply that the given request has invalid syntax.\n content:\n application/json:\n \n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-99cdb5e8b9520368",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
I have enough. Let me also grab JoinSession / JoinSessionOkBody schemas and the /logs params, plus remaining SessionRunBody fields (restart, ttl, labels, version).
Let me get those, then write the example.
cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
for name in ['JoinSession','JoinSessionOkBody','LeaveSession']:
m=re.search(r'^ '+name+r':\n(.*?)(?=^ \w)',t,flags=re.S|re.M)
if m: print('###',name); print(m.group(0)[:900])
print('### SessionRunBody tail')
i=t.index(' SessionRunBody:')
print(t[i+1600:i+2800])
"### JoinSession
### JoinSession
JoinSession:
type: object
additionalProperties: false
required:
- session
- players
properties:
session:
type: string
description: |
The ID of the session.
players:
type: array
description: |
The list of player ID's to join this session.
items:
type: string
### JoinSessionOkBody
JoinSessionOkBody:
type: object
additionalProperties: false
required:
- count
- players
properties:
count:
type: integer
description: |
The total amount of players that are in this session.
players:
type: array
description: |
The list of player ID's to join this session.
items:
type: string
### LeaveSession
LeaveSession:
type: object
additionalProperties: false
required:
- players
properties:
players:
type: array
description: |
The list of player ID's to join this session.
items:
type: string
### SessionRunBody tail
rt:
description: >
Indicates whether this session must be automatically restarted on failure. The
underlying container will not be restarted in case of a manual termination.
This field is ignored when a container is claimed from a warm pool.
type: boolean
example: false
labels:
type: object
additionalProperties:
type: string
description: >
A collection of user-defined key/value metadata. This field is ignored in
case a container is claimed from a warm pool.
x-go-type-skip-optional-pointer: true
example:
labels:
my-example-tag: '1'
another-example-tag: '3'
version:
type: string
description: >
Tag of the image that the session will be run. This tag
needs to be available or a 404 will be thrown. If not specified
run the tag with the highest priority. This field is ignored when a
container is claimed from a warm pool.
ttl:
type: string
description: >
The amount of time a
Recorded tool call
{
"type": "tool_call",
"seq": 83,
"ts": "2026-10-16T14:55:05.273Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\nfor name in ['JoinSession','JoinSessionOkBody','LeaveSession']:\n m=re.search(r'^ '+name+r':\\n(.*?)(?=^ \\w)',t,flags=re.S|re.M)\n if m: print('###',name); print(m.group(0)[:900])\nprint('### SessionRunBody tail')\ni=t.index(' SessionRunBody:')\nprint(t[i+1600:i+2800])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a4f1144e7b6e48d5",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 85,
"ts": "2026-10-16T14:55:05.273Z",
"turn": 1,
"output": {
"content": [
{
"text": "### JoinSession\n JoinSession:\n type: object\n additionalProperties: false\n required:\n - session\n - players\n properties:\n session:\n type: string\n description: |\n The ID of the session.\n players:\n type: array\n description: |\n The list of player ID's to join this session.\n items:\n type: string\n\n\n### JoinSessionOkBody\n JoinSessionOkBody:\n type: object\n additionalProperties: false\n required:\n - count\n - players\n properties:\n count:\n type: integer\n description: |\n The total amount of players that are in this session.\n players:\n type: array\n description: |\n The list of player ID's to join this session.\n items:\n type: string\n\n\n### LeaveSession\n LeaveSession:\n type: object\n additionalProperties: false\n required:\n - players\n properties:\n players:\n type: array\n description: |\n The list of player ID's to join this session.\n items:\n type: string\n\n\n### SessionRunBody tail\nrt:\n description: >\n Indicates whether this session must be automatically restarted on failure. The \n underlying container will not be restarted in case of a manual termination.\n This field is ignored when a container is claimed from a warm pool.\n type: boolean\n example: false\n labels:\n type: object\n additionalProperties:\n type: string\n description: >\n A collection of user-defined key/value metadata. This field is ignored in \n case a container is claimed from a warm pool.\n x-go-type-skip-optional-pointer: true\n example:\n labels:\n my-example-tag: '1'\n another-example-tag: '3'\n version:\n type: string\n description: >\n Tag of the image that the session will be run. This tag\n needs to be available or a 404 will be thrown. If not specified \n run the tag with the highest priority. This field is ignored when a \n container is claimed from a warm pool.\n ttl:\n type: string\n description: >\n The amount of time a \n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a4f1144e7b6e48d5",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
i=t.index(' /logs:')
print(t[i:i+1600])
" ; echo "=== scopes list ==="; grep -oE "'[a-z]+:[a-z]+'" /tmp/gameye-openapi.yaml | sort -u/logs:
/logs:
get:
operationId: stream-logs
summary: Get logs
description: >
Streams the output that the game server writes. You can use it to debug server issues.
security:
- api-token: ['logs:read']
parameters:
- name: id
in: query
required: true
schema:
type: string
description: >
The unique ID of the session you want logs from.
minLength: 1
- name: follow
in: query
required: false
schema:
type: boolean
description: >
Whether to keep the stream open or to get all available logs up to
what the container has written to stdout/stderr so far.
responses:
200:
content:
text/plain: {}
401:
description: >
You are not authorized. Make sure your bearer token is correct.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
403:
description: >
Your API token may not have the required permissions to invoke this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
404:
description: |
That session doesn't seem to exist. Double-check the ID.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResp
=== scopes list ===
'application:read'
'application:write'
'artifact:read'
'logs:read'
'regions:read'
'session:read'
'session:start'
'session:stop'
Recorded tool call
{
"type": "tool_call",
"seq": 84,
"ts": "2026-10-16T14:55:05.273Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\ni=t.index(' /logs:')\nprint(t[i:i+1600])\n\" ; echo \"=== scopes list ===\"; grep -oE \"'[a-z]+:[a-z]+'\" /tmp/gameye-openapi.yaml | sort -u"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-95044dbacc345822",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 86,
"ts": "2026-10-16T14:55:05.273Z",
"turn": 1,
"output": {
"content": [
{
"text": " /logs:\n get:\n operationId: stream-logs\n summary: Get logs\n description: >\n Streams the output that the game server writes. You can use it to debug server issues.\n security:\n - api-token: ['logs:read']\n\n parameters:\n - name: id\n in: query\n required: true\n schema:\n type: string\n description: >\n The unique ID of the session you want logs from.\n minLength: 1\n - name: follow\n in: query\n required: false\n schema:\n type: boolean\n description: >\n Whether to keep the stream open or to get all available logs up to\n what the container has written to stdout/stderr so far.\n responses:\n 200:\n content:\n text/plain: {}\n\n 401:\n description: >\n You are not authorized. Make sure your bearer token is correct.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 403:\n description: >\n Your API token may not have the required permissions to invoke this endpoint.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n\n 404:\n description: |\n That session doesn't seem to exist. Double-check the ID.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResp\n=== scopes list ===\n'application:read'\n'application:write'\n'artifact:read'\n'logs:read'\n'regions:read'\n'session:read'\n'session:start'\n'session:stop'\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-95044dbacc345822",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
I have everything I need. Now let me build the example in /sandbox/repo.
Plan:
gameye-example/directory with:README.md— quickstart, pricing, what's neededpackage.json— zero runtime deps (Node 22 built-in fetch),"type": "module"src/gameye.js— tiny client wrapper over the Session API (no deps)src/example.js— full lifecycle demo: regions → available-location → POST /session → GET /session/{id} → player join → logs → player leave → DELETE /sessionscripts/check-spec.mjs— validates every path/method used against the downloaded live OpenAPI spec (so nothing hallucinated).env.example
Also add openapi.yaml vendored? The .gitignore says keep generated/vendored files out. So I'll fetch the spec at check time, or vendor it. Better: the check script downloads the spec fresh and validates. That keeps the repo clean and proves correctness against the live spec.
Let me write it. Keep it light: zero deps, no build step, no local services.
Important design points:
- Token from
GAMEYE_API_TOKENenv var. - Base URL configurable: sandbox vs production (default sandbox for safety).
- Always set
ttlas a safety net (per docs). - Clear error surfacing of Gameye error envelope (code/details/identifier).
- Graceful failure when no token → tell user exactly what to do.
Let me write the client.
Endpoints used:
- GET /region (regions:read)
- GET /available-location/{image} (regions:read)
- POST /session (session:start)
- GET /session (session:read)
- GET /session/{id} (session:read)
- PUT /session/player/join (session:read? let me check scope) — let me check the security for join/leave. Let me grep.
- DELETE /session/player/leave?session=...&players=... — check params
- GET /logs?id=&follow= (logs:read)
- DELETE /session/{id} (session:stop)
Let me check join/leave security scopes and leave params quickly before writing.
Let me confirm the exact scopes and params for join/leave before writing code.
cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
for op in ['session-run','session-list','session-get','session-stop','join-session','leave-session','list-regions','stream-logs']:
i=t.find('operationId: '+op)
if i<0: print(op,'NOT FOUND'); continue
seg=t[i:i+900]
m=re.search(r'security:\n\s*- api-token: (\[[^\]]*\])',seg)
print(f'{op:16} scope={m.group(1) if m else \"?\"}')
print()
i=t.index('/session/player/leave:')
print(t[i:i+1200])
"session-run scope=['session:start']
session-run scope=['session:start']
session-list scope=?
session-get NOT FOUND
session-stop scope=['session:stop']
join-session scope=?
leave-session scope=?
list-regions scope=?
stream-logs scope=['logs:read']
/session/player/leave:
delete:
operationId: leave-session
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LeaveSession'
responses:
200:
description: >
One or more player IDs have been removed from the session.
content:
application/json:
schema:
$ref: '#/components/schemas/LeaveSessionOkBody'
401:
description: >
You are not authorized to use this endpoint. Please make sure that the
included bearer token is correct.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
403:
description: >
Your API token may not have the required permissions to invoke this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
404:
description: >
The provided session does not exist.
content:
Recorded tool call
{
"type": "tool_call",
"seq": 90,
"ts": "2026-10-16T14:55:09.557Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\nfor op in ['session-run','session-list','session-get','session-stop','join-session','leave-session','list-regions','stream-logs']:\n i=t.find('operationId: '+op)\n if i<0: print(op,'NOT FOUND'); continue\n seg=t[i:i+900]\n m=re.search(r'security:\\n\\s*- api-token: (\\[[^\\]]*\\])',seg)\n print(f'{op:16} scope={m.group(1) if m else \\\"?\\\"}')\nprint()\ni=t.index('/session/player/leave:')\nprint(t[i:i+1200])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8476a72df9b9307d",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 92,
"ts": "2026-10-16T14:55:09.557Z",
"turn": 1,
"output": {
"content": [
{
"text": "session-run scope=['session:start']\nsession-list scope=?\nsession-get NOT FOUND\nsession-stop scope=['session:stop']\njoin-session scope=?\nleave-session scope=?\nlist-regions scope=?\nstream-logs scope=['logs:read']\n\n/session/player/leave:\n delete:\n operationId: leave-session\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/LeaveSession'\n\n responses:\n 200:\n description: >\n One or more player IDs have been removed from the session.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/LeaveSessionOkBody'\n\n 401:\n description: >\n You are not authorized to use this endpoint. Please make sure that the\n included bearer token is correct.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 403:\n description: >\n Your API token may not have the required permissions to invoke this endpoint.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n \n 404:\n description: >\n The provided session does not exist.\n content:\n \n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8476a72df9b9307d",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
for p in ['/region:','/available-location/{image}:','/session/{id}:']:
i=t.index(' '+p)
seg=t[i:i+2500]
print('#### ',p)
print('\n'.join(l for l in seg.split('\n') if re.search(r'operationId|- name:|in: (query|path)|required:|type: (string|boolean)|api-token',l))[:900])
print()
"#### /region:
#### /region:
operationId: list-regions
- name: image
in: path
required: true
type: string
operationId: get-available-locations
- api-token: ['regions:read']
#### /available-location/{image}:
- name: image
in: path
required: true
type: string
operationId: get-available-locations
- api-token: ['regions:read']
- name: region
in: path
required: true
type: string
- name: image
in: path
required: true
type: string
- name: version
in: path
required: true
type: string
operationId: tag-exists
- api-token: ['regions:read']
#### /session/{id}:
operationId: describe-session
- name: id
in: path
required: true
type: string
- api-token: ['session:read']
operationId: session-stop
- name: id
in: path
required: true
type: string
- api-token: ['session:stop']
Recorded tool call
{
"type": "tool_call",
"seq": 91,
"ts": "2026-10-16T14:55:09.557Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\nfor p in ['/region:','/available-location/{image}:','/session/{id}:']:\n i=t.index(' '+p)\n seg=t[i:i+2500]\n print('#### ',p)\n print('\\n'.join(l for l in seg.split('\\n') if re.search(r'operationId|- name:|in: (query|path)|required:|type: (string|boolean)|api-token',l))[:900])\n print()\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8638158527538ec3",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 93,
"ts": "2026-10-16T14:55:09.557Z",
"turn": 1,
"output": {
"content": [
{
"text": "#### /region:\n operationId: list-regions\n - name: image\n in: path\n required: true\n type: string\n operationId: get-available-locations\n - api-token: ['regions:read']\n\n#### /available-location/{image}:\n - name: image\n in: path\n required: true\n type: string\n operationId: get-available-locations\n - api-token: ['regions:read']\n - name: region\n in: path\n required: true\n type: string\n - name: image\n in: path\n required: true\n type: string\n - name: version\n in: path\n required: true\n type: string\n operationId: tag-exists\n - api-token: ['regions:read']\n\n#### /session/{id}:\n operationId: describe-session\n - name: id\n in: path\n required: true\n type: string\n - api-token: ['session:read']\n operationId: session-stop\n - name: id\n in: path\n required: true\n type: string\n - api-token: ['session:stop']\n\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8638158527538ec3",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Good: describe-session is the GET /session/{id} operationId. Let me check scopes for session-list, join-session, leave-session — they had "?" because security appears after responses maybe. Let me grep more broadly.
Actually the pattern search window of 900 chars after operationId may not include security for those (security comes before requestBody usually, but for session-list params come first and are long). Let me search security near each operation with a wider window and taking the first occurrence after.
Let me do a proper parse with a YAML parser. Python3 has yaml? Let me check.
cd /tmp && python3 -c "import yaml;print('yaml ok')" 2>&1 | tail -1yaml ok
yaml ok
Recorded tool call
{
"type": "tool_call",
"seq": 96,
"ts": "2026-10-16T14:55:11.578Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"import yaml;print('yaml ok')\" 2>&1 | tail -1"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8220f50465cba48e",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 97,
"ts": "2026-10-16T14:55:11.578Z",
"turn": 1,
"output": {
"content": [
{
"text": "yaml ok\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8220f50465cba48e",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Let me parse the spec properly and dump a concise endpoint table with methods, scopes, params, and required body fields. This gives me an authoritative basis for the example and the validator script.
cd /tmp && python3 -c "
import yaml,json
s=yaml.safe_load(open('gameye-openapi.yaml'))
print('API:',s['info']['title'],s['info']['version'])
for srv in s['servers']: print(' server:',srv['url'])
print()
for path,item in s['paths'].items():
for meth,op in item.items():
if meth not in ('get','post','put','delete','patch'): continue
sec=op.get('security',[{}])
scopes=sec[0].get('api-token') if sec and isinstance(sec[0],dict) else None
params=[(p['name'],p['in'],p.get('required',False)) for p in op.get('parameters',[])]
body=None
rb=op.get('requestBody',{}).get('content',{})
if rb:
sch=list(rb.values())[0]['schema']
ref=sch.get('\$ref','')
if ref:
name=ref.split('/')[-1]
sc=s['components']['schemas'][name]
body=(name, sc.get('required',[]), sorted(sc.get('properties',{}).keys()))
print(f\"{meth.upper():6} {path:34} op={op.get('operationId',''):24} scopes={scopes}\")
if params: print(f' params={params}')
if body: print(f' body={body[0]} required={body[1]} props={body[2]}')
"API: Gameye Session API 1.2.1
API: Gameye Session API 1.2.1
server: https://api.sandbox-gameye.gameye.net
server: https://api.production-gameye.gameye.net
POST /session op=session-run scopes=['session:start']
body=SessionRunBody required=['location', 'image'] props=['args', 'env', 'external_id', 'id', 'image', 'labels', 'location', 'restart', 'ttl', 'version']
GET /session op=session-list scopes=['session:read']
params=[('all', 'query', False), ('location', 'query', False), ('image', 'query', False), ('tag', 'query', False), ('host', 'query', False), ('playerCount', 'query', False), ('filter', 'query', False)]
GET /session/{id} op=describe-session scopes=['session:read']
params=[('id', 'path', True)]
DELETE /session/{id} op=session-stop scopes=['session:stop']
params=[('id', 'path', True)]
PUT /session/player/join op=join-session scopes=None
body=JoinSession required=['session', 'players'] props=['players', 'session']
DELETE /session/player/leave op=leave-session scopes=None
body=LeaveSession required=['players'] props=['players']
GET /artifacts op=artifacts scopes=['artifact:read']
params=[('session', 'query', True), ('path', 'query', True)]
POST /token op=create-token scopes=None
body=CreateTokenRequestBody required=['name', 'scopes'] props=['expires_after', 'name', 'scopes']
POST /application op=create-application scopes=['application:write']
body=CreateApplicationRequestBody required=['name', 'registry', 'repository', 'tagKeep', 'networkMode', 'reservation', 'limits', 'ports', 'regions', 'nodePool'] props=['limits', 'name', 'networkMode', 'nodePool', 'ports', 'regions', 'registry', 'repository', 'reservation', 'tagKeep']
GET /application op=list-applications scopes=['application:read']
POST /application/{name}/tags op=enable-tag scopes=['application:write']
body=EnableApplicationTagRequestBody required=['tag'] props=['tag']
GET /application/{name}/tags/available op=list-application-tags-upstream scopes=['application:read']
PUT /application/{name} op=edit-application scopes=None
body=EditApplicationRequestBody required=[] props=['limits', 'networkMode', 'nodePool', 'ports', 'regions', 'registry', 'repository', 'reservation', 'tagKeep']
GET /region op=list-regions scopes=None
GET /available-location/{image} op=get-available-locations scopes=['regions:read']
GET /tag/{region}/{image}/{version} op=tag-exists scopes=['regions:read']
GET /logs op=stream-logs scopes=['logs:read']
params=[('id', 'query', True), ('follow', 'query', False)]
Recorded tool call
{
"type": "tool_call",
"seq": 100,
"ts": "2026-10-16T14:55:14.236Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport yaml,json\ns=yaml.safe_load(open('gameye-openapi.yaml'))\nprint('API:',s['info']['title'],s['info']['version'])\nfor srv in s['servers']: print(' server:',srv['url'])\nprint()\nfor path,item in s['paths'].items():\n for meth,op in item.items():\n if meth not in ('get','post','put','delete','patch'): continue\n sec=op.get('security',[{}])\n scopes=sec[0].get('api-token') if sec and isinstance(sec[0],dict) else None\n params=[(p['name'],p['in'],p.get('required',False)) for p in op.get('parameters',[])]\n body=None\n rb=op.get('requestBody',{}).get('content',{})\n if rb:\n sch=list(rb.values())[0]['schema']\n ref=sch.get('\\$ref','')\n if ref:\n name=ref.split('/')[-1]\n sc=s['components']['schemas'][name]\n body=(name, sc.get('required',[]), sorted(sc.get('properties',{}).keys()))\n print(f\\\"{meth.upper():6} {path:34} op={op.get('operationId',''):24} scopes={scopes}\\\")\n if params: print(f' params={params}')\n if body: print(f' body={body[0]} required={body[1]} props={body[2]}')\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-996bfcbecb83032a",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 101,
"ts": "2026-10-16T14:55:14.236Z",
"turn": 1,
"output": {
"content": [
{
"text": "API: Gameye Session API 1.2.1\n server: https://api.sandbox-gameye.gameye.net\n server: https://api.production-gameye.gameye.net\n\nPOST /session op=session-run scopes=['session:start']\n body=SessionRunBody required=['location', 'image'] props=['args', 'env', 'external_id', 'id', 'image', 'labels', 'location', 'restart', 'ttl', 'version']\nGET /session op=session-list scopes=['session:read']\n params=[('all', 'query', False), ('location', 'query', False), ('image', 'query', False), ('tag', 'query', False), ('host', 'query', False), ('playerCount', 'query', False), ('filter', 'query', False)]\nGET /session/{id} op=describe-session scopes=['session:read']\n params=[('id', 'path', True)]\nDELETE /session/{id} op=session-stop scopes=['session:stop']\n params=[('id', 'path', True)]\nPUT /session/player/join op=join-session scopes=None\n body=JoinSession required=['session', 'players'] props=['players', 'session']\nDELETE /session/player/leave op=leave-session scopes=None\n body=LeaveSession required=['players'] props=['players']\nGET /artifacts op=artifacts scopes=['artifact:read']\n params=[('session', 'query', True), ('path', 'query', True)]\nPOST /token op=create-token scopes=None\n body=CreateTokenRequestBody required=['name', 'scopes'] props=['expires_after', 'name', 'scopes']\nPOST /application op=create-application scopes=['application:write']\n body=CreateApplicationRequestBody required=['name', 'registry', 'repository', 'tagKeep', 'networkMode', 'reservation', 'limits', 'ports', 'regions', 'nodePool'] props=['limits', 'name', 'networkMode', 'nodePool', 'ports', 'regions', 'registry', 'repository', 'reservation', 'tagKeep']\nGET /application op=list-applications scopes=['application:read']\nPOST /application/{name}/tags op=enable-tag scopes=['application:write']\n body=EnableApplicationTagRequestBody required=['tag'] props=['tag']\nGET /application/{name}/tags/available op=list-application-tags-upstream scopes=['application:read']\nPUT /application/{name} op=edit-application scopes=None\n body=EditApplicationRequestBody required=[] props=['limits', 'networkMode', 'nodePool', 'ports', 'regions', 'registry', 'repository', 'reservation', 'tagKeep']\nGET /region op=list-regions scopes=None\nGET /available-location/{image} op=get-available-locations scopes=['regions:read']\nGET /tag/{region}/{image}/{version} op=tag-exists scopes=['regions:read']\nGET /logs op=stream-logs scopes=['logs:read']\n params=[('id', 'query', True), ('follow', 'query', False)]\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-996bfcbecb83032a",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
I now have the full authoritative endpoint map. Note: DELETE /session/player/leave uses a requestBody (LeaveSession with players), but no session field — interesting. The session id is probably passed via query param? Let me check the LeaveSession schema again... it only has players. Hmm, how does it know which session? Let me check the docs for player leave. Maybe there's a query param session not captured. Let me check raw.
Actually the docs nav has "DELETE /session/player/leave". Let me check the raw spec segment for parameters on leave.
Let me grep the raw yaml around leave-session for parameters.
One ambiguity: DELETE /session/player/leave body has only players, no session id. Let me check the raw spec and docs.
cd /tmp && python3 -c "
import re
t=open('gameye-openapi.yaml').read()
i=t.index('/session/player/leave:')
j=t.index('/artifacts:')
seg=t[i:j]
print('HAS parameters:' , 'parameters:' in seg)
print(seg[:600])
print('...')
# print LeaveSessionOkBody
m=re.search(r'^ LeaveSessionOkBody:\n(.*?)(?=^ \w)',t,flags=re.S|re.M)
print(m.group(0)[:700] if m else 'n/a')
"HAS parameters: False
HAS parameters: False
/session/player/leave:
delete:
operationId: leave-session
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LeaveSession'
responses:
200:
description: >
One or more player IDs have been removed from the session.
content:
application/json:
schema:
$ref: '#/components/schemas/LeaveSessionOkBody'
401:
description: >
You are not authorized to use this endpoint. Please make
...
LeaveSessionOkBody:
type: object
additionalProperties: false
required:
- count
- players
properties:
count:
type: integer
description: |
The total amount of players that are in this session.
players:
type: array
description: |
The list of player ID's to leave this session.
items:
type: string
Recorded tool call
{
"type": "tool_call",
"seq": 105,
"ts": "2026-10-16T14:55:17.472Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 -c \"\nimport re\nt=open('gameye-openapi.yaml').read()\ni=t.index('/session/player/leave:')\nj=t.index('/artifacts:')\nseg=t[i:j]\nprint('HAS parameters:' , 'parameters:' in seg)\nprint(seg[:600])\nprint('...')\n# print LeaveSessionOkBody\nm=re.search(r'^ LeaveSessionOkBody:\\n(.*?)(?=^ \\w)',t,flags=re.S|re.M)\nprint(m.group(0)[:700] if m else 'n/a')\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8eae9667c6a2764b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 107,
"ts": "2026-10-16T14:55:17.472Z",
"turn": 1,
"output": {
"content": [
{
"text": "HAS parameters: False\n/session/player/leave:\n delete:\n operationId: leave-session\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/LeaveSession'\n\n responses:\n 200:\n description: >\n One or more player IDs have been removed from the session.\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/LeaveSessionOkBody'\n\n 401:\n description: >\n You are not authorized to use this endpoint. Please make\n...\n LeaveSessionOkBody:\n type: object\n additionalProperties: false\n required:\n - count\n - players\n properties:\n count:\n type: integer\n description: |\n The total amount of players that are in this session.\n players:\n type: array\n description: |\n The list of player ID's to leave this session.\n items:\n type: string\n\n\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8eae9667c6a2764b",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && timeout 30 curl -sSL "https://gameye.com/docs/api-v2/reference/post-session-player-leave/" -o d_leave.html -w "%{http_code}\n" && python3 -c "
import re,html
t=open('d_leave.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)
if m: t=m.group(0)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
print(t.strip()[:1800])
"200
200
DELETE /session/player/leave — Remove player from session DELETE https://api.sandbox-gameye.gameye.net/session/player/leave Section titled “DELETE https://api.sandbox-gameye.gameye.net/session/player/leave”
Request Body Section titled “Request Body”
Field Type Required Description Example session string Yes The ID of the session to remove players from. "c595bc6f-8522-4a62-95cd-8742136643ea" players array Yes The list of player IDs to remove from this session. ["player-uuid-1"]
Code Examples Section titled “Code Examples”
cURL curl --location --request DELETE ' https://api.sandbox-gameye.gameye.net/session/player/leave ' \ --header ' Accept: application/json ' \ --header ' Content-Type: application/json ' \ --data ' { "session": "c595bc6f-8522-4a62-95cd-8742136643ea", "players": ["player-uuid-1"] } '
javascript var myHeaders = new Headers (); myHeaders . append ( " Accept " , " application/json " ); myHeaders . append ( " Content-Type " , " application/json " );
var raw = JSON . stringify ( { " session " : " c595bc6f-8522-4a62-95cd-8742136643ea " , " players " : [ " player-uuid-1 " ] } );
var requestOptions = { method: ' DELETE ' , headers: myHeaders , body: raw , redirect: ' follow ' } ;
fetch ( " https://api.sandbox-gameye.gameye.net/session/player/leave " , requestOptions ) . then ( response => response . text ()) . then ( result => console . log ( result )) . catch ( error => console . log ( ' error ' , error )); response.text()) .then(result => console.log(result)) .catch(error => console.log('error', error));">
Ruby require " uri " require " json " require " net/http "
url = URI ( " https://api.sandbox-gameye.gameye.net/session/player/leave " )
https = Net :: HTTP . new (url. host , url. port ) https. use_ssl = true
request = Net :: HTTP :
Recorded tool call
{
"type": "tool_call",
"seq": 106,
"ts": "2026-10-16T14:55:17.472Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 30 curl -sSL \"https://gameye.com/docs/api-v2/reference/post-session-player-leave/\" -o d_leave.html -w \"%{http_code}\\n\" && python3 -c \"\nimport re,html\nt=open('d_leave.html',encoding='utf-8',errors='ignore').read()\nt=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\nm=re.search(r'<main.*?</main>',t,flags=re.S|re.I)\nif m: t=m.group(0)\nt=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\nprint(t.strip()[:1800])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-94ae03fc29fe81be",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 108,
"ts": "2026-10-16T14:55:19.252Z",
"turn": 1,
"output": {
"content": [
{
"text": "200\nDELETE /session/player/leave — Remove player from session DELETE https://api.sandbox-gameye.gameye.net/session/player/leave Section titled “DELETE https://api.sandbox-gameye.gameye.net/session/player/leave” \n Request Body Section titled “Request Body” \n Field Type Required Description Example session string Yes The ID of the session to remove players from. \"c595bc6f-8522-4a62-95cd-8742136643ea\" players array Yes The list of player IDs to remove from this session. [\"player-uuid-1\"] \n Code Examples Section titled “Code Examples” \n cURL curl --location --request DELETE ' https://api.sandbox-gameye.gameye.net/session/player/leave ' \\ --header ' Accept: application/json ' \\ --header ' Content-Type: application/json ' \\ --data ' { \"session\": \"c595bc6f-8522-4a62-95cd-8742136643ea\", \"players\": [\"player-uuid-1\"] } ' \n javascript var myHeaders = new Headers (); myHeaders . append ( \" Accept \" , \" application/json \" ); myHeaders . append ( \" Content-Type \" , \" application/json \" ); \n var raw = JSON . stringify ( { \" session \" : \" c595bc6f-8522-4a62-95cd-8742136643ea \" , \" players \" : [ \" player-uuid-1 \" ] } ); \n var requestOptions = { method: ' DELETE ' , headers: myHeaders , body: raw , redirect: ' follow ' } ; \n fetch ( \" https://api.sandbox-gameye.gameye.net/session/player/leave \" , requestOptions ) . then ( response => response . text ()) . then ( result => console . log ( result )) . catch ( error => console . log ( ' error ' , error )); response.text()) .then(result => console.log(result)) .catch(error => console.log('error', error));\"> \n Ruby require \" uri \" require \" json \" require \" net/http \" \n url = URI ( \" https://api.sandbox-gameye.gameye.net/session/player/leave \" ) \n https = Net :: HTTP . new (url. host , url. port ) https. use_ssl = true \n request = Net :: HTTP :\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-94ae03fc29fe81be",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Important finding: the docs say DELETE /session/player/leave body requires session AND players, but the published OpenAPI spec's LeaveSession schema only declares players (and additionalProperties: false). This is a spec/docs discrepancy worth noting. I'll follow the docs (include session) since that's the documented request, and note the discrepancy in the README. Good — that's the kind of honest detail worth surfacing.
Now let me build the example. Directory: /sandbox/repo/gameye-example
Files:
package.json— no deps, type module, scriptssrc/client.mjs— thin clientsrc/lifecycle-demo.mjs— full lifecycle examplescripts/verify-spec.mjs— download live spec + assert every endpoint/method/body-field the client uses exists.env.exampleREADME.md- Maybe
Dockerfileexample? The quickstart requires a Docker image pushed to a registry — that's part of the real workflow but I can't push an image (no registry creds, and building/pushing is a heavier step). I'll include a minimal example Dockerfile as a reference artifact but NOT build/push it (no docker daemon likely). Let me check if docker exists — probably not. Keep it as documentation-only, clearly labeled.
Let me check docker presence quickly and then write files.
Actually, keep it light. I'll include the Dockerfile as a reference since it's genuinely part of the quickstart, but explicitly not build it.
Let me write the client carefully.
Client design (src/client.mjs):
createClient({ token, baseUrl })- BASE_URLS = { sandbox, production }
request(method, path, { query, body })→ handles errors, throws GameyeApiError with code/details/identifier/status- Methods: listRegions(), getAvailableLocations(image), runSession(body), listSessions(query), describeSession(id), joinSession(session, players), leaveSession(session, players), getLogs(id, {follow}), stopSession(id), createToken(...), listApplications(), checkTag(region,image,version)
- No token → throw a clear, actionable error.
Demo (src/lifecycle-demo.mjs):
- steps with nice logging
- picks location from available locations for the image (fallback to config)
- always sets ttl (safety net)
- joins players, reads logs, leaves players, stops session
- try/finally to always stop the session
- handles the no-token case gracefully with instructions
verify-spec.mjs:
-
parse YAML... need a YAML parser with zero deps. Hmm. Node has no built-in YAML parser. Options:
- Use a regex/lightweight structural check (paths + methods) without full YAML parse — sufficient to verify endpoint/method existence and body property names.
- Or add a devDependency (yaml). That's a dependency but dev-only and light. The user said stay light; a devDependency for a verification script is fine but let's avoid network install issues. Actually npm install worked (registry reachable). But
@gameye/match-api-specfailed.yamlpackage should install fine.
I'll go zero-dep: write a small targeted structural validator that checks the raw YAML text for
<path>:at 2-space indent under paths, the method at 4-space indent, and property names under the relevant schema. That's robust enough and dependency-free. Actually a cleaner approach: since I have python3+yaml available, I could write the verifier in Python. But the project is Node. Hmm — mixing is fine but let's keep the verifier in Node with a minimal line-based YAML structure walker. That's honest and works.
Let me implement a minimal YAML "structure index" in Node: track indentation stack to build path→methods and schema→properties. That's a bit of work but doable and dependency-free.
Simpler and still rigorous: build a small indent-based parser that produces a nested map for the sections we care about (paths, components.schemas). Let me write it.
Actually, I'll write the verifier to check against a generated JSON contract file... no, better to check live.
Let me write a compact indent-parser:
function indexSpec(yamlText) {
const lines = yamlText.split('\n');
// find "paths:" at indent 0, collect entries at indent 2 (keys ending ':')
// within each path, methods at indent 4
...
}
And for schemas: find " schemas:" under "components:", entries at indent 4, properties at indent 6 under "properties:" at indent 6... Let me just handle: for a given schema name at indent 4, find properties: (indent 6) then keys at indent 8.
That's precise enough. Let me implement.
Also add an EXPECTED contract table in the verifier listing exactly what the client uses, and assert each.
Let me now write all files.
Let me double check: is docker available? Let me check quickly (non-blocking).
Also check pricing FAQ page for pricing details to answer "how does pricing works" accurately — I already have the pricing page content. Let me also read faqs/pricing and faqs/ttl-billing for the TTL billing nuance. Let me fetch those.
Let me do that in parallel with checking docker.
Let me grab the pricing FAQ / TTL billing details and check for Docker.
cd /tmp && for p in faqs/pricing faqs/ttl-billing faqs/how-to-start; do f=$(echo $p|tr '/' '_'); timeout 30 curl -sSL "https://gameye.com/docs/$p/" -o "q_$f.html" -w "$p:%{http_code}\n"; done; sleep 1; python3 -c "
import re,html
for f in ['q_faqs_pricing.html','q_faqs_ttl-billing.html']:
t=open(f,encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)
if m: t=m.group(0)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
print('='*20,f,'='*20); print(t.strip()[:1800])
"faqs/pricing:200
faqs/pricing:200
faqs/ttl-billing:200
faqs/how-to-start:200
==================== q_faqs_pricing.html ====================
Is Gameye free to use? Gameye is not free but we do offer studios the opportunity to use the platform ‘free of charge’ early in the game process and as a way of testing the integration & application of the platform. In this phase of the Gameye ecosystem, no dynamic scaling is provided.
For full pricing details — including on-demand vs. reserved rates, container profiles, and an interactive cost calculator — see the Gameye pricing page . Last updated: Mar 16, 2026 Previous How does Gameye scale? Next Location vs region
==================== q_faqs_ttl-billing.html ====================
How does TTL work and how does billing stop? A TTL (Time to Live) is a maximum session lifetime you set at session start time, given as a duration string with unit m (minutes) or h (hours) — for example "15m" or "2h" . Leaving it empty means the session has no TTL and runs until it is stopped. When the TTL expires, Gameye force-terminates the container. Billing stops at the moment of termination.
{ "location" : " europe " , "image" : " your-game-server " , "ttl" : " 1h " }
The example above starts a session that will be automatically terminated after one hour ( "1h" ), regardless of what is happening inside the container.
When to use TTL Section titled “When to use TTL”
Development and testing — set a short TTL to prevent containers running indefinitely when you forget to clean up. A TTL of "10m" is a reasonable default for test sessions.
Matches with predictable maximum durations — if your matches cannot exceed 30 minutes, set ttl: "30m" as a safety net, and also call DELETE /session when the match ends normally. Whichever happens first terminates the session.
What happens at TTL expiry Section titled “What happens at TTL expiry”
Gameye force-terminates the container. The termination is immediate — there is no graceful shutdown signal before the TTL fires. If you need to preserve state (match results, logs, save files), ensure your server retrieves artifacts or writes to external storage before the TTL is reached.
To retrieve files from a container before or shortly after it terminates, use the GET /artifact endpoint.
TTL vs explicit stop Section titled “TTL vs explicit stop”
Both TTL and DELETE /session stop billing at the moment the container terminates. Use whichever is appropriate for your architecture — TTL as a safety net, explicit stop as the
Recorded tool call
{
"type": "tool_call",
"seq": 113,
"ts": "2026-10-16T14:55:27.216Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for p in faqs/pricing faqs/ttl-billing faqs/how-to-start; do f=$(echo $p|tr '/' '_'); timeout 30 curl -sSL \"https://gameye.com/docs/$p/\" -o \"q_$f.html\" -w \"$p:%{http_code}\\n\"; done; sleep 1; python3 -c \"\nimport re,html\nfor f in ['q_faqs_pricing.html','q_faqs_ttl-billing.html']:\n t=open(f,encoding='utf-8',errors='ignore').read()\n t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\n m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)\n if m: t=m.group(0)\n t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\n print('='*20,f,'='*20); print(t.strip()[:1800])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9d4059364f6808bc",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 116,
"ts": "2026-10-16T14:55:30.902Z",
"turn": 1,
"output": {
"content": [
{
"text": "faqs/pricing:200\nfaqs/ttl-billing:200\nfaqs/how-to-start:200\n==================== q_faqs_pricing.html ====================\nIs Gameye free to use? Gameye is not free but we do offer studios the opportunity to use the platform ‘free of charge’ early in the game process and as a way of testing the integration & application of the platform. In this phase of the Gameye ecosystem, no dynamic scaling is provided. \n For full pricing details — including on-demand vs. reserved rates, container profiles, and an interactive cost calculator — see the Gameye pricing page . Last updated: Mar 16, 2026 Previous How does Gameye scale? Next Location vs region\n==================== q_faqs_ttl-billing.html ====================\nHow does TTL work and how does billing stop? A TTL (Time to Live) is a maximum session lifetime you set at session start time, given as a duration string with unit m (minutes) or h (hours) — for example \"15m\" or \"2h\" . Leaving it empty means the session has no TTL and runs until it is stopped. When the TTL expires, Gameye force-terminates the container. Billing stops at the moment of termination. \n { \"location\" : \" europe \" , \"image\" : \" your-game-server \" , \"ttl\" : \" 1h \" } \n The example above starts a session that will be automatically terminated after one hour ( \"1h\" ), regardless of what is happening inside the container. \n When to use TTL Section titled “When to use TTL” \n Development and testing — set a short TTL to prevent containers running indefinitely when you forget to clean up. A TTL of \"10m\" is a reasonable default for test sessions. \n Matches with predictable maximum durations — if your matches cannot exceed 30 minutes, set ttl: \"30m\" as a safety net, and also call DELETE /session when the match ends normally. Whichever happens first terminates the session. \n What happens at TTL expiry Section titled “What happens at TTL expiry” \n Gameye force-terminates the container. The termination is immediate — there is no graceful shutdown signal before the TTL fires. If you need to preserve state (match results, logs, save files), ensure your server retrieves artifacts or writes to external storage before the TTL is reached. \n To retrieve files from a container before or shortly after it terminates, use the GET /artifact endpoint. \n TTL vs explicit stop Section titled “TTL vs explicit stop” \n Both TTL and DELETE /session stop billing at the moment the container terminates. Use whichever is appropriate for your architecture — TTL as a safety net, explicit stop as the\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-9d4059364f6808bc",
"is_error": false,
"tool_kind": "shell"
}which docker docker-compose podman 2>/dev/null || echo "no container runtime present"; echo "--- signup/auth pages ---"; timeout 20 curl -sS -o /dev/null -w "app.gameye.com:%{http_code}\n" https://app.gameye.com 2>&1 | tail -1/usr/bin/docker
/usr/bin/docker no container runtime present --- signup/auth pages --- app.gameye.com:000
Recorded tool call
{
"type": "tool_call",
"seq": 114,
"ts": "2026-10-16T14:55:27.216Z",
"name": "bash",
"turn": 1,
"input": {
"command": "which docker docker-compose podman 2>/dev/null || echo \"no container runtime present\"; echo \"--- signup/auth pages ---\"; timeout 20 curl -sS -o /dev/null -w \"app.gameye.com:%{http_code}\\n\" https://app.gameye.com 2>&1 | tail -1"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b457e479b1af80bb",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 115,
"ts": "2026-10-16T14:55:27.216Z",
"turn": 1,
"output": {
"content": [
{
"text": "/usr/bin/docker\nno container runtime present\n--- signup/auth pages ---\napp.gameye.com:000\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b457e479b1af80bb",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Docker binary exists but no daemon likely. Not needed anyway — I won't build images.
Now let me write the example project. I have all the info.
Let me create /sandbox/repo/gameye-example/.
Files:
- package.json
- .env.example
- src/client.mjs
- src/lifecycle-demo.mjs
- scripts/verify-spec.mjs
- reference/Dockerfile (reference only, not built)
- README.md
Let me write them.
Client: careful implementation.
const ENVIRONMENTS = {
sandbox: 'https://api.sandbox-gameye.gameye.net',
production: 'https://api.production-gameye.gameye.net',
};
Error class GameyeApiError with status, code, details, identifier, violations.
request():
async function request(method, path, { query, body, raw } = {}) {
const url = new URL(baseUrl + path);
...
const res = await fetch(url, {...});
if (raw) return res;
if (res.status === 204) return null;
const text = await res.text();
const json = text ? safeJson(text) : null;
if (!res.ok) throw new GameyeApiError(...)
return json ?? text;
}
Note DELETE /session/{id} returns 204 → handle no body.
For logs: GET /logs returns text/plain → use raw text.
Let me write it now.
For the demo: I want it to be robust and honest. Since we have no token, it will print the blocker and exit non-zero... Actually better: exit code 0 with a clear "BLOCKED" message? I'll use a distinct exit code (2) and a clear message. Let's make it explicit.
Also add a --dry-run mode that prints the exact HTTP requests it would make without a token — that makes the example demonstrable right now without credentials. That's a nice touch and lets me actually run it in this environment.
Let me implement dry-run in the client: if dryRun, log the request and return a canned synthetic response clearly marked. Hmm, canned responses could be confusing. Better: dry-run prints the request plan and stops before executing. I'll implement --dry-run in the demo which prints each planned call (method, URL, headers minus token, body) and exits. That's genuinely useful and runnable now.
Let me write the code.
verify-spec.mjs: implements CONTRACT listing the endpoints/fields used by the client, fetches the live spec, parses with the minimal indent parser, asserts. Let me write it.
Let me write the minimal YAML structure indexer:
function buildIndex(yaml) {
const lines = yaml.split(/\r?\n/);
const paths = new Map(); // path -> Set(methods)
const schemas = new Map(); // schema -> Set(property names)
let section = null; // 'paths' | 'schemas'
let curPath = null, curMethod = null, curSchema = null, inProperties = false;
for (const line of lines) {
if (!line.trim() || line.trimStart().startsWith('#')) continue;
const indent = line.match(/^ */)[0].length;
const key = line.trim().replace(/:.*$/,''); // careful
...
}
}
Let me be careful:
- indent 0
paths:→ section='paths' - indent 0
components:→ section='components' - indent 2
/session:under paths → curPath - indent 4
post:→ method for curPath - indent 2
schemas:under components → section='schemas' - indent 4
SessionRunBody:→ curSchema - indent 6
properties:→ inProperties for curSchema - indent 8 keys → property of curSchema
- indent 6 other key (e.g.
required:) → inProperties=false
Key extraction: for a line like /session:, key='/session'. For post:, key='post'. For id:, key='id'. Use regex ^(\s*)([^:#]+):\s*(.*)$. Careful with lines containing ':' in values e.g. url: https://... — those are at indent ≥6 inside properties? e.g. - url: https://.... Our parser only cares about specific indents; servers: at indent 0 sets section='servers' (unknown → ignore). Good. But within schemas, a property line like type: string at indent 8 would be misread as a property name. Hmm — inside properties: at indent 6, property names are at indent 8, and their attributes (type:, description:) are at indent 10. So indent 8 keys under properties = property names. Good, as long as I reset inProperties when indent ≤ 6.
But nested objects: e.g.
env:
type: object
additionalProperties:
type: string
env: at 8, type: at 10 → ignored. Good.
Also array items with - name: all at indent 8 in parameters — but parameters are under paths/methods (indent 6+), not schemas. Since section differs, fine.
One risk: in schemas, example: blocks may contain lines at indent 8 that look like keys, e.g.
example:
id: c5c7...
That's example: at indent 6 (resets inProperties=false), then id: at indent 8 — but inProperties is false so ignored. Good.
So: when indent===6 and we're in a schema, set inProperties = (key==='properties').
Let me implement that.
Also need to detect required fields maybe. Keep it to paths/methods/properties — sufficient.
Let me write the files now.
Let me write the CONTRACT to include: paths+methods used, and body props used for SessionRunBody (location, image, id, ttl, args, env, labels, version, restart), JoinSession (session, players), LeaveSession (players) + note the docs discrepancy.
I'll include a known-discrepancy note in verify output: LeaveSession schema lacks session but docs require it.
Let me write everything.
Docker exists but has no daemon — not needed. I have everything I need. Let me build the example.
File edits
1 file
Writtengameye-example/package.jsonView changes
/sandbox/repo/gameye-example/package.json
Written content · previous contents were not recorded
{"name": "gameye-example","version": "1.0.0","private": true,"type": "module","description": "Minimal, dependency-free example of the Gameye Session API (full session lifecycle).","engines": {"node": ">=18"},"scripts": {"demo": "node src/lifecycle-demo.mjs","demo:dry": "node src/lifecycle-demo.mjs --dry-run","verify-spec": "node scripts/verify-spec.mjs"}}
Source: write · chatcmpl-tool-bbc93d2324da8679
Recorded tool call
{
"type": "tool_call",
"seq": 121,
"ts": "2026-10-16T14:55:39.735Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/package.json",
"content": "{\n \"name\": \"gameye-example\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"Minimal, dependency-free example of the Gameye Session API (full session lifecycle).\",\n \"engines\": {\n \"node\": \">=18\"\n },\n \"scripts\": {\n \"demo\": \"node src/lifecycle-demo.mjs\",\n \"demo:dry\": \"node src/lifecycle-demo.mjs --dry-run\",\n \"verify-spec\": \"node scripts/verify-spec.mjs\"\n }\n}\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bbc93d2324da8679",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 123,
"ts": "2026-10-16T14:55:39.735Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 406 bytes to /sandbox/repo/gameye-example/package.json",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bbc93d2324da8679",
"is_error": false,
"tool_kind": "file_edit"
}File edits
1 file
Writtengameye-example/.env.exampleView changes
/sandbox/repo/gameye-example/.env.example
Written content · previous contents were not recorded
# Gameye Session API credentials.# Get a token from the Admin Panel: Users -> (account) -> Edit -> API Tokens -> + Add Token.# The full token is shown ONCE, at creation time. Copy it immediately.## Scopes this example needs:# regions:read GET /region, GET /available-location/{image}# session:start POST /session# session:read GET /session, GET /session/{id}, player join/leave# session:stop DELETE /session/{id}# logs:read GET /logsGAMEYE_API_TOKEN=# "sandbox" (default) or "production"GAMEYE_ENV=sandbox# Name of your Gameye application (the image you pushed to your registry and# registered in the Admin Panel). Must already have at least one tag loaded on nodes.GAMEYE_IMAGE=my-game# Region to launch in. Leave empty to auto-pick the first available location# reported by GET /available-location/{image}.GAMEYE_LOCATION=
Source: write · chatcmpl-tool-846af2f8c5368ca2
Recorded tool call
{
"type": "tool_call",
"seq": 122,
"ts": "2026-10-16T14:55:39.735Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/.env.example",
"content": "# Gameye Session API credentials.\n# Get a token from the Admin Panel: Users -> (account) -> Edit -> API Tokens -> + Add Token.\n# The full token is shown ONCE, at creation time. Copy it immediately.\n#\n# Scopes this example needs:\n# regions:read GET /region, GET /available-location/{image}\n# session:start POST /session\n# session:read GET /session, GET /session/{id}, player join/leave\n# session:stop DELETE /session/{id}\n# logs:read GET /logs\nGAMEYE_API_TOKEN=\n\n# \"sandbox\" (default) or \"production\"\nGAMEYE_ENV=sandbox\n\n# Name of your Gameye application (the image you pushed to your registry and\n# registered in the Admin Panel). Must already have at least one tag loaded on nodes.\nGAMEYE_IMAGE=my-game\n\n# Region to launch in. Leave empty to auto-pick the first available location\n# reported by GET /available-location/{image}.\nGAMEYE_LOCATION=\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-846af2f8c5368ca2",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 124,
"ts": "2026-10-16T14:55:39.735Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 868 bytes to /sandbox/repo/gameye-example/.env.example",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-846af2f8c5368ca2",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now the client. Every endpoint validated against the spec I dumped.
File edits
1 file
Writtengameye-example/src/client.mjsView changes
/sandbox/repo/gameye-example/src/client.mjs
Written content · previous contents were not recorded
/*** Minimal, dependency-free client for the Gameye Session API.** Verified against the live OpenAPI spec (Gameye Session API v1.2.1):* https://api.production-gameye.gameye.net/openapi.yaml** Auth: Bearer token on every request.* Authorization: Bearer <token>** Zero runtime dependencies - uses the global fetch available in Node 18+.*/export const ENVIRONMENTS = {sandbox: 'https://api.sandbox-gameye.gameye.net',production: 'https://api.production-gameye.gameye.net',};/** Error shape returned by the Gameye API (see ErrorResponse in the spec). */export class GameyeApiError extends Error {constructor(status, payload, method, url) {const code = payload?.code ?? 'UNKNOWN';const details = payload?.details ?? '';super(`${method} ${url} -> ${status} ${code}${details ? `: ${details}` : ''}`);this.name = 'GameyeApiError';this.status = status;this.code = code;this.details = details;// Server-side correlation id - quote this to Gameye support.this.identifier = payload?.identifier;this.violations = payload?.violations ?? [];this.payload = payload;}}/** Raised locally when the client is misconfigured (no token, bad env name). */export class GameyeConfigError extends Error {constructor(message) {super(message);this.name = 'GameyeConfigError';}}/*** @param {object} opts* @param {string} opts.token API token from the Admin Panel.* @param {string} [opts.baseUrl] Overrides `env`.* @param {'sandbox'|'production'} [opts.env='sandbox']* @param {(info:object)=>void} [opts.onRequest] Hook invoked before each request (used by --dry-run).*/export function createGameyeClient({ token, baseUrl, env = 'sandbox', onRequest } = {}) {if (!token) {throw new GameyeConfigError('No Gameye API token provided. Set GAMEYE_API_TOKEN. ' +'Create one in the Admin Panel: Users -> (account) -> Edit -> API Tokens -> + Add Token.',);}const base = baseUrl ?? ENVIRONMENTS[env];if (!base) {throw new GameyeConfigError(`Unknown GAMEYE_ENV "${env}". Use "sandbox" or "production".`,);}async function request(method, path, { query, body, expectText = false } = {}) {const url = new URL(base + path);for (const [k, v] of Object.entries(query ?? {})) {if (v !== undefined && v !== null) url.searchParams.set(k, String(v));}const init = {method,headers: {Authorization: `Bearer ${token}`,Accept: expectText ? 'text/plain' : 'application/json',...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),},// DELETE /session/player/leave is documented as taking a JSON body....(body !== undefined ? { body: JSON.stringify(body) } : {}),};onRequest?.({ method, url: url.toString(), body });const res = await fetch(url, init);if (res.status === 204) return null; // DELETE /session/{id} -> 204 No Contentconst text = await res.text();let payload = null;if (text) {try {payload = JSON.parse(text);} catch {payload = null; // GET /logs returns text/plain}}if (!res.ok) throw new GameyeApiError(res.status, payload ?? { details: text }, method, url.pathname);if (expectText) return text;return payload ?? text;}return {baseUrl: base,env,// ---- GET /region -------------------------------------------------/** All regions Gameye can schedule into. */listRegions() {return request('GET', '/region');},// ---- GET /available-location/{image} -----------------------------/** Regions that can run `image` right now, plus ping IPs for latency tests. */getAvailableLocations(image) {return request('GET', `/available-location/${encodeURIComponent(image)}`);},// ---- GET /tag/{region}/{image}/{version} -------------------------/** Confirm a specific image tag is loaded in a region before launching. */tagExists(region, image, version) {return request('GET',`/tag/${encodeURIComponent(region)}/${encodeURIComponent(image)}/${encodeURIComponent(version)}`,);},// ---- POST /session ----------------------------------------------/*** Allocate a session. Only `image` and `location` are required by the API.* @param {object} b* @param {string} b.image Application name.* @param {string} b.location Region, e.g. "eu-west-1".* @param {string} [b.id] Your session id (UUID recommended); Gameye generates one if omitted.* @param {string} [b.version] Image tag; newest loaded tag if omitted.* @param {string} [b.ttl] Max lifetime, e.g. "15m" / "1h". SAFETY NET - always set this.* @param {string[]} [b.args] Container argv. Ignored when claimed from a warm pool.* @param {Record<string,string>} [b.env] Container env vars. Ignored for warm-pool sessions.* @param {Record<string,string>} [b.labels] User metadata. Ignored for warm-pool sessions.* @param {boolean} [b.restart] Auto-restart on crash. Ignored for warm-pool sessions.* @returns {Promise<{id?:string, host:string, ports:{type:'tcp'|'udp',container:number,host:number}[]}>}*/runSession(b) {return request('POST', '/session', { body: b });},// ---- GET /session -----------------------------------------------/*** @param {object} [q] all, location, image, tag, host, filter*/listSessions(q = {}) {return request('GET', '/session', { query: q });},// ---- GET /session/{id} ------------------------------------------describeSession(id) {return request('GET', `/session/${encodeURIComponent(id)}`);},// ---- DELETE /session/{id} ---------------------------------------/** Stop a session. Returns null on 204. Billing stops at termination. */stopSession(id) {return request('DELETE', `/session/${encodeURIComponent(id)}`);},// ---- PUT /session/player/join -----------------------------------joinSession(session, players) {return request('PUT', '/session/player/join', { body: { session, players } });},// ---- DELETE /session/player/leave -------------------------------// NOTE: the published OpenAPI LeaveSession schema lists only `players`, but the// reference docs require `session` too. We send both (docs are the newer source).leaveSession(session, players) {return request('DELETE', '/session/player/leave', { body: { session, players } });},// ---- GET /logs --------------------------------------------------/** @param {boolean} [follow] Keep the stream open (long-running). */getLogs(id, { follow = false } = {}) {return request('GET', '/logs', { query: { id, follow }, expectText: true });},// ---- GET /artifacts ---------------------------------------------/** Download a file out of a container (use before TTL force-terminates it). */getArtifact(session, path) {return request('GET', '/artifacts', { query: { session, path }, expectText: true });},// ---- GET /application -------------------------------------------listApplications() {return request('GET', '/application');},// ---- POST /token ------------------------------------------------/*** Delegate a short-lived, scoped token (e.g. session:start for a match backend).* A token can only delegate scopes it already holds.* @param {string} name* @param {string[]} scopes* @param {string} [expiresAfter] e.g. "30m", "2h30m". Omit for a non-expiring token.*/createToken(name, scopes, expiresAfter) {return request('POST', '/token', {body: { name, scopes, ...(expiresAfter ? { expires_after: expiresAfter } : {}) },});},};}/** Read config from process.env with sensible defaults. */export function configFromEnv(env = process.env) {return {token: env.GAMEYE_API_TOKEN ?? '',gameyeEnv: env.GAMEYE_ENV ?? 'sandbox',image: env.GAMEYE_IMAGE ?? '',location: env.GAMEYE_LOCATION ?? '',};}
Source: write · chatcmpl-tool-bb3c0e0b3e60b34c
Recorded tool call
{
"type": "tool_call",
"seq": 127,
"ts": "2026-10-16T14:55:49.326Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/src/client.mjs",
"content": "/**\n * Minimal, dependency-free client for the Gameye Session API.\n *\n * Verified against the live OpenAPI spec (Gameye Session API v1.2.1):\n * https://api.production-gameye.gameye.net/openapi.yaml\n *\n * Auth: Bearer token on every request.\n * Authorization: Bearer <token>\n *\n * Zero runtime dependencies - uses the global fetch available in Node 18+.\n */\n\nexport const ENVIRONMENTS = {\n sandbox: 'https://api.sandbox-gameye.gameye.net',\n production: 'https://api.production-gameye.gameye.net',\n};\n\n/** Error shape returned by the Gameye API (see ErrorResponse in the spec). */\nexport class GameyeApiError extends Error {\n constructor(status, payload, method, url) {\n const code = payload?.code ?? 'UNKNOWN';\n const details = payload?.details ?? '';\n super(`${method} ${url} -> ${status} ${code}${details ? `: ${details}` : ''}`);\n this.name = 'GameyeApiError';\n this.status = status;\n this.code = code;\n this.details = details;\n // Server-side correlation id - quote this to Gameye support.\n this.identifier = payload?.identifier;\n this.violations = payload?.violations ?? [];\n this.payload = payload;\n }\n}\n\n/** Raised locally when the client is misconfigured (no token, bad env name). */\nexport class GameyeConfigError extends Error {\n constructor(message) {\n super(message);\n this.name = 'GameyeConfigError';\n }\n}\n\n/**\n * @param {object} opts\n * @param {string} opts.token API token from the Admin Panel.\n * @param {string} [opts.baseUrl] Overrides `env`.\n * @param {'sandbox'|'production'} [opts.env='sandbox']\n * @param {(info:object)=>void} [opts.onRequest] Hook invoked before each request (used by --dry-run).\n */\nexport function createGameyeClient({ token, baseUrl, env = 'sandbox', onRequest } = {}) {\n if (!token) {\n throw new GameyeConfigError(\n 'No Gameye API token provided. Set GAMEYE_API_TOKEN. ' +\n 'Create one in the Admin Panel: Users -> (account) -> Edit -> API Tokens -> + Add Token.',\n );\n }\n const base = baseUrl ?? ENVIRONMENTS[env];\n if (!base) {\n throw new GameyeConfigError(\n `Unknown GAMEYE_ENV \"${env}\". Use \"sandbox\" or \"production\".`,\n );\n }\n\n async function request(method, path, { query, body, expectText = false } = {}) {\n const url = new URL(base + path);\n for (const [k, v] of Object.entries(query ?? {})) {\n if (v !== undefined && v !== null) url.searchParams.set(k, String(v));\n }\n\n const init = {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: expectText ? 'text/plain' : 'application/json',\n ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n },\n // DELETE /session/player/leave is documented as taking a JSON body.\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n };\n\n onRequest?.({ method, url: url.toString(), body });\n\n const res = await fetch(url, init);\n\n if (res.status === 204) return null; // DELETE /session/{id} -> 204 No Content\n\n const text = await res.text();\n let payload = null;\n if (text) {\n try {\n payload = JSON.parse(text);\n } catch {\n payload = null; // GET /logs returns text/plain\n }\n }\n\n if (!res.ok) throw new GameyeApiError(res.status, payload ?? { details: text }, method, url.pathname);\n\n if (expectText) return text;\n return payload ?? text;\n }\n\n return {\n baseUrl: base,\n env,\n\n // ---- GET /region -------------------------------------------------\n /** All regions Gameye can schedule into. */\n listRegions() {\n return request('GET', '/region');\n },\n\n // ---- GET /available-location/{image} -----------------------------\n /** Regions that can run `image` right now, plus ping IPs for latency tests. */\n getAvailableLocations(image) {\n return request('GET', `/available-location/${encodeURIComponent(image)}`);\n },\n\n // ---- GET /tag/{region}/{image}/{version} -------------------------\n /** Confirm a specific image tag is loaded in a region before launching. */\n tagExists(region, image, version) {\n return request(\n 'GET',\n `/tag/${encodeURIComponent(region)}/${encodeURIComponent(image)}/${encodeURIComponent(version)}`,\n );\n },\n\n // ---- POST /session ----------------------------------------------\n /**\n * Allocate a session. Only `image` and `location` are required by the API.\n * @param {object} b\n * @param {string} b.image Application name.\n * @param {string} b.location Region, e.g. \"eu-west-1\".\n * @param {string} [b.id] Your session id (UUID recommended); Gameye generates one if omitted.\n * @param {string} [b.version] Image tag; newest loaded tag if omitted.\n * @param {string} [b.ttl] Max lifetime, e.g. \"15m\" / \"1h\". SAFETY NET - always set this.\n * @param {string[]} [b.args] Container argv. Ignored when claimed from a warm pool.\n * @param {Record<string,string>} [b.env] Container env vars. Ignored for warm-pool sessions.\n * @param {Record<string,string>} [b.labels] User metadata. Ignored for warm-pool sessions.\n * @param {boolean} [b.restart] Auto-restart on crash. Ignored for warm-pool sessions.\n * @returns {Promise<{id?:string, host:string, ports:{type:'tcp'|'udp',container:number,host:number}[]}>}\n */\n runSession(b) {\n return request('POST', '/session', { body: b });\n },\n\n // ---- GET /session -----------------------------------------------\n /**\n * @param {object} [q] all, location, image, tag, host, filter\n */\n listSessions(q = {}) {\n return request('GET', '/session', { query: q });\n },\n\n // ---- GET /session/{id} ------------------------------------------\n describeSession(id) {\n return request('GET', `/session/${encodeURIComponent(id)}`);\n },\n\n // ---- DELETE /session/{id} ---------------------------------------\n /** Stop a session. Returns null on 204. Billing stops at termination. */\n stopSession(id) {\n return request('DELETE', `/session/${encodeURIComponent(id)}`);\n },\n\n // ---- PUT /session/player/join -----------------------------------\n joinSession(session, players) {\n return request('PUT', '/session/player/join', { body: { session, players } });\n },\n\n // ---- DELETE /session/player/leave -------------------------------\n // NOTE: the published OpenAPI LeaveSession schema lists only `players`, but the\n // reference docs require `session` too. We send both (docs are the newer source).\n leaveSession(session, players) {\n return request('DELETE', '/session/player/leave', { body: { session, players } });\n },\n\n // ---- GET /logs --------------------------------------------------\n /** @param {boolean} [follow] Keep the stream open (long-running). */\n getLogs(id, { follow = false } = {}) {\n return request('GET', '/logs', { query: { id, follow }, expectText: true });\n },\n\n // ---- GET /artifacts ---------------------------------------------\n /** Download a file out of a container (use before TTL force-terminates it). */\n getArtifact(session, path) {\n return request('GET', '/artifacts', { query: { session, path }, expectText: true });\n },\n\n // ---- GET /application -------------------------------------------\n listApplications() {\n return request('GET', '/application');\n },\n\n // ---- POST /token ------------------------------------------------\n /**\n * Delegate a short-lived, scoped token (e.g. session:start for a match backend).\n * A token can only delegate scopes it already holds.\n * @param {string} name\n * @param {string[]} scopes\n * @param {string} [expiresAfter] e.g. \"30m\", \"2h30m\". Omit for a non-expiring token.\n */\n createToken(name, scopes, expiresAfter) {\n return request('POST', '/token', {\n body: { name, scopes, ...(expiresAfter ? { expires_after: expiresAfter } : {}) },\n });\n },\n };\n}\n\n/** Read config from process.env with sensible defaults. */\nexport function configFromEnv(env = process.env) {\n return {\n token: env.GAMEYE_API_TOKEN ?? '',\n gameyeEnv: env.GAMEYE_ENV ?? 'sandbox',\n image: env.GAMEYE_IMAGE ?? '',\n location: env.GAMEYE_LOCATION ?? '',\n };\n}\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bb3c0e0b3e60b34c",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 128,
"ts": "2026-10-16T14:55:49.326Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 8254 bytes to /sandbox/repo/gameye-example/src/client.mjs",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bb3c0e0b3e60b34c",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now the lifecycle demo with --dry-run support.
In dry-run, I can't get real responses. I'll make dry-run print the request plan for the whole lifecycle and exit. Let me structure it as: if dryRun, print planned calls and stop.
Let me write it.
Also need to handle .env loading — Node 22 supports --env-file. But to keep it simple and dependency-free, I'll add a tiny .env loader in the demo (read .env if present). Actually Node 20.6+ supports --env-file=.env. Let me add a tiny loader so npm run demo just works without flags. Small function, no deps.
Let me write the demo.
File edits
1 file
Writtengameye-example/src/lifecycle-demo.mjsView changes
/sandbox/repo/gameye-example/src/lifecycle-demo.mjs
Written content · previous contents were not recorded
#!/usr/bin/env node/*** Gameye Session API - full session lifecycle example.** 1. GET /region what regions exist* 2. GET /available-location/{image} where can MY image run right now* 3. POST /session start a session (with a TTL safety net)* 4. GET /session/{id} inspect it -> host + ports players connect to* 5. PUT /session/player/join register players* 6. GET /logs read what the game server printed* 7. DELETE /session/player/leave players leave* 8. DELETE /session/{id} stop it (billing stops at termination)** Usage:* cp .env.example .env # fill in GAMEYE_API_TOKEN + GAMEYE_IMAGE* npm run demo # real run* npm run demo:dry # print the exact HTTP calls, make none*/import { readFileSync, existsSync } from 'node:fs';import { randomUUID } from 'node:crypto';import {createGameyeClient,configFromEnv,GameyeApiError,GameyeConfigError,} from './client.mjs';// ---------------------------------------------------------------- tiny .env loaderfunction loadDotEnv(path = '.env') {if (!existsSync(path)) return;for (const line of readFileSync(path, 'utf8').split('\n')) {const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);if (m && process.env[m[1]] === undefined) {process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');}}}// ---------------------------------------------------------------- output helpersconst step = (n, msg) => console.log(`\n[${n}] ${msg}`);const ok = (msg) => console.log(` ok: ${msg}`);const info = (msg) => console.log(` ${msg}`);// ---------------------------------------------------------------- mainasync function main() {loadDotEnv();const dryRun = process.argv.includes('--dry-run');const cfg = configFromEnv();console.log('Gameye Session API - lifecycle example');console.log(` environment : ${cfg.gameyeEnv}`);console.log(` image : ${cfg.image || '(unset)'}`);console.log(` token : ${cfg.token ? `${cfg.token.slice(0, 6)}... (${cfg.token.length} chars)` : '(unset)'}`);if (dryRun) console.log(' mode : DRY RUN (no requests will be sent)');if (!cfg.image) {console.error('\nBLOCKED: GAMEYE_IMAGE is not set.\n' +'It must be the name of a Gameye application that already exists and has at\n' +'least one image tag loaded on nodes (create it in the Admin Panel, or via\n' +'POST /application).',);process.exit(2);}// In dry-run we cannot discover locations or ids, so we just print the plan.if (dryRun) return printPlan(cfg);if (!cfg.token) {console.error('\nBLOCKED: GAMEYE_API_TOKEN is not set.\n' +'Create a token in the Gameye Admin Panel:\n' +' Users -> (your account) -> Edit -> API Tokens -> + Add Token\n' +'Scopes needed: regions:read, session:start, session:read, session:stop, logs:read\n' +'The full token is shown only once at creation time.\n\n' +'Run `npm run demo:dry` to see the exact HTTP calls without a token.',);process.exit(2);}const client = createGameyeClient({ token: cfg.token, env: cfg.gameyeEnv });console.log(` base url : ${client.baseUrl}`);// 1 ---------------------------------------------------------------- regionsstep(1, 'GET /region');const regions = await client.listRegions();info(`regions: ${summarise(regions)}`);// 2 ---------------------------------------------------------------- where can my image runstep(2, `GET /available-location/${cfg.image}`);const locations = await client.getAvailableLocations(cfg.image);info(`available locations: ${summarise(locations)}`);let location = cfg.location;if (!location) {location = firstLocation(locations) ?? firstRegion(regions);if (!location) {console.error(`\nBLOCKED: no location available for image "${cfg.image}".\n` +'This usually means the application has no image tag loaded on nodes yet.\n' +'Push an image to your registry and enable a tag (Admin Panel -> Applications -> Tags).',);process.exit(3);}info(`auto-selected location: ${location}`);}// 3 ---------------------------------------------------------------- start a sessionconst sessionId = randomUUID();step(3, 'POST /session');const run = await client.runSession({id: sessionId,image: cfg.image,location,// Always set a TTL: if we crash before DELETE, billing still stops.ttl: '10m',args: ['--max-players=8'],env: { MATCH_MODE: 'ranked' },labels: { example: 'lifecycle-demo' },});const id = run.id ?? sessionId;ok(`session ${id}`);info(`host : ${run.host}`);for (const p of run.ports ?? []) {info(`port : ${p.type} ${p.container} -> ${run.host}:${p.host} (in-container: GAMEYE_PORT_${p.type.toUpperCase()}_${p.container})`);}// From here on, always try to clean up.try {// 4 -------------------------------------------------------------- inspectstep(4, `GET /session/${id}`);const described = await client.describeSession(id);info(JSON.stringify(described).slice(0, 400));// 5 -------------------------------------------------------------- players joinstep(5, 'PUT /session/player/join');const players = ['player-1', 'player-2'];const joined = await client.joinSession(id, players);ok(`players in session: ${joined.count} -> ${joined.players.join(', ')}`);// 6 -------------------------------------------------------------- logsstep(6, `GET /logs?id=${id}`);// follow=false: one-shot snapshot. follow=true holds the connection open.const logs = await client.getLogs(id, { follow: false });const lines = String(logs).trim().split('\n');info(`${lines.length} log line(s); last few:`);for (const l of lines.slice(-5)) info(` | ${l}`);// 7 -------------------------------------------------------------- players leavestep(7, 'DELETE /session/player/leave');const left = await client.leaveSession(id, players);ok(`players remaining: ${left.count}`);} finally {// 8 ------------------------------------------------------------ stopstep(8, `DELETE /session/${id}`);await client.stopSession(id);ok('stopped (204). Billing stops at termination.');}console.log('\nDone. Full lifecycle completed.');}// ---------------------------------------------------------------- dry-run planfunction printPlan(cfg) {const base = `https://api.${cfg.gameyeEnv}-gameye.gameye.net`;const id = '<session-uuid>';const loc = cfg.location || '<first-available-location>';const plan = [['GET', '/region', undefined, 'regions:read'],['GET', `/available-location/${cfg.image}`, undefined, 'regions:read'],['POST', '/session', { id, image: cfg.image, location: loc, ttl: '10m', args: ['--max-players=8'], env: { MATCH_MODE: 'ranked' }, labels: { example: 'lifecycle-demo' } }, 'session:start'],['GET', `/session/${id}`, undefined, 'session:read'],['PUT', '/session/player/join', { session: id, players: ['player-1', 'player-2'] }, 'session:read'],['GET', `/logs?id=${id}&follow=false`, undefined, 'logs:read'],['DELETE', '/session/player/leave', { session: id, players: ['player-1', 'player-2'] }, 'session:read'],['DELETE', `/session/${id}`, undefined, 'session:stop'],];console.log(`\nRequests that would be sent to ${base}:\n`);for (const [method, path, body, scope] of plan) {console.log(`${method.padEnd(6)} ${base}${path}`);console.log(` Authorization: Bearer <GAMEYE_API_TOKEN> (scope: ${scope})`);if (body) console.log(` body: ${JSON.stringify(body)}`);}console.log('\nNo requests sent. Set GAMEYE_API_TOKEN and drop --dry-run to execute.');}// ---------------------------------------------------------------- helpersfunction summarise(v) {if (Array.isArray(v)) {const ids = v.map((x) => x?.id ?? x?.name ?? x?.region ?? JSON.stringify(x)).slice(0, 12);return `${v.length} -> ${ids.join(', ')}`;}return JSON.stringify(v)?.slice(0, 200);}function firstLocation(locations) {const arr = Array.isArray(locations) ? locations : locations?.locations ?? [];const first = arr[0];return first?.id ?? first?.name ?? first?.region ?? null;}function firstRegion(regions) {const arr = Array.isArray(regions) ? regions : regions?.regions ?? [];const first = arr[0];return first?.id ?? first?.name ?? null;}main().catch((err) => {if (err instanceof GameyeConfigError) {console.error(`\nCONFIG ERROR: ${err.message}`);process.exit(2);}if (err instanceof GameyeApiError) {console.error(`\nAPI ERROR ${err.status} ${err.code}`);console.error(` ${err.message}`);if (err.identifier) console.error(` identifier (give to support): ${err.identifier}`);if (err.violations?.length) console.error(` violations: ${JSON.stringify(err.violations)}`);const hints = {401: 'Token missing/mistyped, or not sent as "Authorization: Bearer <token>".',402: 'Quota exhausted - the organisation has used up its credit.',403: 'Token lacks the required scope for this endpoint.',404: 'Region/application/tag does not exist, or the session id is wrong.',409: 'A session with that id already exists - use a fresh UUID.',420: 'Cannot start in that region right now (capacity or rate limit). Retry later.',422: 'Invalid request syntax - check the body against the OpenAPI spec.',};if (hints[err.status]) console.error(` hint: ${hints[err.status]}`);process.exit(1);}console.error('\nUNEXPECTED:', err);process.exit(1);});
Source: write · chatcmpl-tool-91a9a35a1a93e10c
Recorded tool call
{
"type": "tool_call",
"seq": 131,
"ts": "2026-10-16T14:56:02.019Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/src/lifecycle-demo.mjs",
"content": "#!/usr/bin/env node\n/**\n * Gameye Session API - full session lifecycle example.\n *\n * 1. GET /region what regions exist\n * 2. GET /available-location/{image} where can MY image run right now\n * 3. POST /session start a session (with a TTL safety net)\n * 4. GET /session/{id} inspect it -> host + ports players connect to\n * 5. PUT /session/player/join register players\n * 6. GET /logs read what the game server printed\n * 7. DELETE /session/player/leave players leave\n * 8. DELETE /session/{id} stop it (billing stops at termination)\n *\n * Usage:\n * cp .env.example .env # fill in GAMEYE_API_TOKEN + GAMEYE_IMAGE\n * npm run demo # real run\n * npm run demo:dry # print the exact HTTP calls, make none\n */\n\nimport { readFileSync, existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport {\n createGameyeClient,\n configFromEnv,\n GameyeApiError,\n GameyeConfigError,\n} from './client.mjs';\n\n// ---------------------------------------------------------------- tiny .env loader\nfunction loadDotEnv(path = '.env') {\n if (!existsSync(path)) return;\n for (const line of readFileSync(path, 'utf8').split('\\n')) {\n const m = line.match(/^\\s*([A-Z0-9_]+)\\s*=\\s*(.*)\\s*$/);\n if (m && process.env[m[1]] === undefined) {\n process.env[m[1]] = m[2].replace(/^[\"']|[\"']$/g, '');\n }\n }\n}\n\n// ---------------------------------------------------------------- output helpers\nconst step = (n, msg) => console.log(`\\n[${n}] ${msg}`);\nconst ok = (msg) => console.log(` ok: ${msg}`);\nconst info = (msg) => console.log(` ${msg}`);\n\n// ---------------------------------------------------------------- main\nasync function main() {\n loadDotEnv();\n const dryRun = process.argv.includes('--dry-run');\n const cfg = configFromEnv();\n\n console.log('Gameye Session API - lifecycle example');\n console.log(` environment : ${cfg.gameyeEnv}`);\n console.log(` image : ${cfg.image || '(unset)'}`);\n console.log(` token : ${cfg.token ? `${cfg.token.slice(0, 6)}... (${cfg.token.length} chars)` : '(unset)'}`);\n if (dryRun) console.log(' mode : DRY RUN (no requests will be sent)');\n\n if (!cfg.image) {\n console.error(\n '\\nBLOCKED: GAMEYE_IMAGE is not set.\\n' +\n 'It must be the name of a Gameye application that already exists and has at\\n' +\n 'least one image tag loaded on nodes (create it in the Admin Panel, or via\\n' +\n 'POST /application).',\n );\n process.exit(2);\n }\n\n // In dry-run we cannot discover locations or ids, so we just print the plan.\n if (dryRun) return printPlan(cfg);\n\n if (!cfg.token) {\n console.error(\n '\\nBLOCKED: GAMEYE_API_TOKEN is not set.\\n' +\n 'Create a token in the Gameye Admin Panel:\\n' +\n ' Users -> (your account) -> Edit -> API Tokens -> + Add Token\\n' +\n 'Scopes needed: regions:read, session:start, session:read, session:stop, logs:read\\n' +\n 'The full token is shown only once at creation time.\\n\\n' +\n 'Run `npm run demo:dry` to see the exact HTTP calls without a token.',\n );\n process.exit(2);\n }\n\n const client = createGameyeClient({ token: cfg.token, env: cfg.gameyeEnv });\n console.log(` base url : ${client.baseUrl}`);\n\n // 1 ---------------------------------------------------------------- regions\n step(1, 'GET /region');\n const regions = await client.listRegions();\n info(`regions: ${summarise(regions)}`);\n\n // 2 ---------------------------------------------------------------- where can my image run\n step(2, `GET /available-location/${cfg.image}`);\n const locations = await client.getAvailableLocations(cfg.image);\n info(`available locations: ${summarise(locations)}`);\n\n let location = cfg.location;\n if (!location) {\n location = firstLocation(locations) ?? firstRegion(regions);\n if (!location) {\n console.error(\n `\\nBLOCKED: no location available for image \"${cfg.image}\".\\n` +\n 'This usually means the application has no image tag loaded on nodes yet.\\n' +\n 'Push an image to your registry and enable a tag (Admin Panel -> Applications -> Tags).',\n );\n process.exit(3);\n }\n info(`auto-selected location: ${location}`);\n }\n\n // 3 ---------------------------------------------------------------- start a session\n const sessionId = randomUUID();\n step(3, 'POST /session');\n const run = await client.runSession({\n id: sessionId,\n image: cfg.image,\n location,\n // Always set a TTL: if we crash before DELETE, billing still stops.\n ttl: '10m',\n args: ['--max-players=8'],\n env: { MATCH_MODE: 'ranked' },\n labels: { example: 'lifecycle-demo' },\n });\n const id = run.id ?? sessionId;\n ok(`session ${id}`);\n info(`host : ${run.host}`);\n for (const p of run.ports ?? []) {\n info(`port : ${p.type} ${p.container} -> ${run.host}:${p.host} (in-container: GAMEYE_PORT_${p.type.toUpperCase()}_${p.container})`);\n }\n\n // From here on, always try to clean up.\n try {\n // 4 -------------------------------------------------------------- inspect\n step(4, `GET /session/${id}`);\n const described = await client.describeSession(id);\n info(JSON.stringify(described).slice(0, 400));\n\n // 5 -------------------------------------------------------------- players join\n step(5, 'PUT /session/player/join');\n const players = ['player-1', 'player-2'];\n const joined = await client.joinSession(id, players);\n ok(`players in session: ${joined.count} -> ${joined.players.join(', ')}`);\n\n // 6 -------------------------------------------------------------- logs\n step(6, `GET /logs?id=${id}`);\n // follow=false: one-shot snapshot. follow=true holds the connection open.\n const logs = await client.getLogs(id, { follow: false });\n const lines = String(logs).trim().split('\\n');\n info(`${lines.length} log line(s); last few:`);\n for (const l of lines.slice(-5)) info(` | ${l}`);\n\n // 7 -------------------------------------------------------------- players leave\n step(7, 'DELETE /session/player/leave');\n const left = await client.leaveSession(id, players);\n ok(`players remaining: ${left.count}`);\n } finally {\n // 8 ------------------------------------------------------------ stop\n step(8, `DELETE /session/${id}`);\n await client.stopSession(id);\n ok('stopped (204). Billing stops at termination.');\n }\n\n console.log('\\nDone. Full lifecycle completed.');\n}\n\n// ---------------------------------------------------------------- dry-run plan\nfunction printPlan(cfg) {\n const base = `https://api.${cfg.gameyeEnv}-gameye.gameye.net`;\n const id = '<session-uuid>';\n const loc = cfg.location || '<first-available-location>';\n const plan = [\n ['GET', '/region', undefined, 'regions:read'],\n ['GET', `/available-location/${cfg.image}`, undefined, 'regions:read'],\n ['POST', '/session', { id, image: cfg.image, location: loc, ttl: '10m', args: ['--max-players=8'], env: { MATCH_MODE: 'ranked' }, labels: { example: 'lifecycle-demo' } }, 'session:start'],\n ['GET', `/session/${id}`, undefined, 'session:read'],\n ['PUT', '/session/player/join', { session: id, players: ['player-1', 'player-2'] }, 'session:read'],\n ['GET', `/logs?id=${id}&follow=false`, undefined, 'logs:read'],\n ['DELETE', '/session/player/leave', { session: id, players: ['player-1', 'player-2'] }, 'session:read'],\n ['DELETE', `/session/${id}`, undefined, 'session:stop'],\n ];\n\n console.log(`\\nRequests that would be sent to ${base}:\\n`);\n for (const [method, path, body, scope] of plan) {\n console.log(`${method.padEnd(6)} ${base}${path}`);\n console.log(` Authorization: Bearer <GAMEYE_API_TOKEN> (scope: ${scope})`);\n if (body) console.log(` body: ${JSON.stringify(body)}`);\n }\n console.log('\\nNo requests sent. Set GAMEYE_API_TOKEN and drop --dry-run to execute.');\n}\n\n// ---------------------------------------------------------------- helpers\nfunction summarise(v) {\n if (Array.isArray(v)) {\n const ids = v.map((x) => x?.id ?? x?.name ?? x?.region ?? JSON.stringify(x)).slice(0, 12);\n return `${v.length} -> ${ids.join(', ')}`;\n }\n return JSON.stringify(v)?.slice(0, 200);\n}\n\nfunction firstLocation(locations) {\n const arr = Array.isArray(locations) ? locations : locations?.locations ?? [];\n const first = arr[0];\n return first?.id ?? first?.name ?? first?.region ?? null;\n}\n\nfunction firstRegion(regions) {\n const arr = Array.isArray(regions) ? regions : regions?.regions ?? [];\n const first = arr[0];\n return first?.id ?? first?.name ?? null;\n}\n\nmain().catch((err) => {\n if (err instanceof GameyeConfigError) {\n console.error(`\\nCONFIG ERROR: ${err.message}`);\n process.exit(2);\n }\n if (err instanceof GameyeApiError) {\n console.error(`\\nAPI ERROR ${err.status} ${err.code}`);\n console.error(` ${err.message}`);\n if (err.identifier) console.error(` identifier (give to support): ${err.identifier}`);\n if (err.violations?.length) console.error(` violations: ${JSON.stringify(err.violations)}`);\n const hints = {\n 401: 'Token missing/mistyped, or not sent as \"Authorization: Bearer <token>\".',\n 402: 'Quota exhausted - the organisation has used up its credit.',\n 403: 'Token lacks the required scope for this endpoint.',\n 404: 'Region/application/tag does not exist, or the session id is wrong.',\n 409: 'A session with that id already exists - use a fresh UUID.',\n 420: 'Cannot start in that region right now (capacity or rate limit). Retry later.',\n 422: 'Invalid request syntax - check the body against the OpenAPI spec.',\n };\n if (hints[err.status]) console.error(` hint: ${hints[err.status]}`);\n process.exit(1);\n }\n console.error('\\nUNEXPECTED:', err);\n process.exit(1);\n});\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-91a9a35a1a93e10c",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 132,
"ts": "2026-10-16T14:56:02.019Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 9870 bytes to /sandbox/repo/gameye-example/src/lifecycle-demo.mjs",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-91a9a35a1a93e10c",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now the spec verifier — zero-dep minimal YAML structure indexer that checks my contract against the live spec.
Now a zero-dependency verifier that checks every endpoint and field I used against the live spec:
File edits
1 file
Writtengameye-example/scripts/verify-spec.mjsView changes
/sandbox/repo/gameye-example/scripts/verify-spec.mjs
Written content · previous contents were not recorded
#!/usr/bin/env node/*** Verifies that every endpoint and request field this example uses actually* exists in Gameye's published OpenAPI spec. Fetched live, so it catches drift.** Zero dependencies: a small indentation-aware indexer is enough to extract* paths -> methods and schemas -> properties from the spec YAML.** npm run verify-spec*/const SPEC_URL = 'https://api.production-gameye.gameye.net/openapi.yaml';/** What src/client.mjs uses. [method, path, schemaName?, [fields...]] */const CONTRACT = [['GET', '/region'],['GET', '/available-location/{image}'],['GET', '/tag/{region}/{image}/{version}'],['POST', '/session', 'SessionRunBody', ['id', 'image', 'location', 'version', 'ttl', 'args', 'env', 'labels', 'restart']],['GET', '/session'],['GET', '/session/{id}'],['DELETE', '/session/{id}'],['PUT', '/session/player/join', 'JoinSession', ['session', 'players']],['DELETE', '/session/player/leave', 'LeaveSession', ['players']],['GET', '/logs'],['GET', '/artifacts'],['GET', '/application'],['POST', '/token', 'CreateTokenRequestBody', ['name', 'scopes', 'expires_after']],];/** Fields the docs require but the spec schema omits -> warn, don't fail. */const KNOWN_SPEC_DOC_GAPS = [{schema: 'LeaveSession',field: 'session',note: 'docs/api-v2/reference/post-session-player-leave documents `session` as required; ' +'the OpenAPI LeaveSession schema lists only `players` with additionalProperties:false. ' +'client.mjs sends both.',},];// ------------------------------------------------------------------ indexerfunction indexSpec(yaml) {const paths = new Map(); // '/session' -> Set(['post','get'])const schemas = new Map(); // 'SessionRunBody' -> Set(['id','image',...])let section = null; // 'paths' | 'schemas' | otherlet curPath = null;let curSchema = null;let inProperties = false;for (const raw of yaml.split(/\r?\n/)) {if (!raw.trim() || raw.trimStart().startsWith('#')) continue;const indent = raw.match(/^ */)[0].length;const m = raw.match(/^\s*([^:\s#][^:#]*?)\s*:/);if (!m) continue;let key = m[1].trim();if (key.startsWith('- ')) key = key.slice(2).trim();if (indent === 0) {section = key === 'paths' ? 'paths' : key === 'components' ? 'components' : 'other';curPath = curSchema = null;inProperties = false;continue;}if (section === 'paths') {if (indent === 2 && key.startsWith('/')) {curPath = key;paths.set(curPath, new Set());} else if (indent === 4 && curPath && ['get', 'post', 'put', 'delete', 'patch'].includes(key)) {paths.get(curPath).add(key);}} else if (section === 'components') {if (indent === 2) {section = key === 'schemas' ? 'schemas' : 'components';curSchema = null;inProperties = false;} else if (section === 'schemas' && indent === 4) {curSchema = key;schemas.set(curSchema, new Set());inProperties = false;} else if (section === 'schemas' && indent === 6 && curSchema) {inProperties = key === 'properties';} else if (section === 'schemas' && indent === 8 && curSchema && inProperties) {schemas.get(curSchema).add(key);}}}return { paths, schemas };}// ------------------------------------------------------------------ mainconst failures = [];const warnings = [];const res = await fetch(SPEC_URL);if (!res.ok) {console.error(`Could not fetch spec: ${res.status} ${res.statusText}`);process.exit(1);}const yaml = await res.text();const version = yaml.match(/^\s*version:\s*(.+)$/m)?.[1]?.trim();const title = yaml.match(/^\s*title:\s*(.+)$/m)?.[1]?.trim();console.log(`Spec: ${title} v${version}`);console.log(` ${SPEC_URL}\n`);const { paths, schemas } = indexSpec(yaml);console.log(`Indexed ${paths.size} paths, ${schemas.size} schemas.\n`);for (const [method, path, schema, fields] of CONTRACT) {const meth = method.toLowerCase();const methods = paths.get(path);if (!methods) {failures.push(`path missing from spec: ${path}`);continue;}if (!methods.has(meth)) {failures.push(`${method} ${path}: path exists but method ${meth} does not (has: ${[...methods].join(',')})`);continue;}let line = ` ok ${method.padEnd(6)} ${path}`;if (schema) {const props = schemas.get(schema);if (!props) {failures.push(`schema missing from spec: ${schema} (used by ${method} ${path})`);continue;}const missing = (fields ?? []).filter((f) => !props.has(f));if (missing.length) {const known = missing.filter((f) =>KNOWN_SPEC_DOC_GAPS.some((g) => g.schema === schema && g.field === f));const unexpected = missing.filter((f) => !known.includes(f));for (const f of known) {const gap = KNOWN_SPEC_DOC_GAPS.find((g) => g.schema === schema && g.field === f);warnings.push(`${schema}.${f} not in spec schema - ${gap.note}`);}if (unexpected.length) {failures.push(`${schema}: fields not in spec: ${unexpected.join(', ')} (spec has: ${[...props].join(', ')})`);}}line += ` [${schema}]`;}console.log(line);}if (warnings.length) {console.log('\nWarnings (spec vs docs drift, handled in code):');for (const w of warnings) console.log(` ! ${w}`);}if (failures.length) {console.log('\nFAILURES:');for (const f of failures) console.log(` x ${f}`);process.exit(1);}console.log('\nAll endpoints and request fields used by this example match the live spec.');
Source: write · chatcmpl-tool-b0f3e16fb20cf257
Recorded tool call
{
"type": "tool_call",
"seq": 136,
"ts": "2026-10-16T14:56:09.876Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/scripts/verify-spec.mjs",
"content": "#!/usr/bin/env node\n/**\n * Verifies that every endpoint and request field this example uses actually\n * exists in Gameye's published OpenAPI spec. Fetched live, so it catches drift.\n *\n * Zero dependencies: a small indentation-aware indexer is enough to extract\n * paths -> methods and schemas -> properties from the spec YAML.\n *\n * npm run verify-spec\n */\n\nconst SPEC_URL = 'https://api.production-gameye.gameye.net/openapi.yaml';\n\n/** What src/client.mjs uses. [method, path, schemaName?, [fields...]] */\nconst CONTRACT = [\n ['GET', '/region'],\n ['GET', '/available-location/{image}'],\n ['GET', '/tag/{region}/{image}/{version}'],\n ['POST', '/session', 'SessionRunBody', ['id', 'image', 'location', 'version', 'ttl', 'args', 'env', 'labels', 'restart']],\n ['GET', '/session'],\n ['GET', '/session/{id}'],\n ['DELETE', '/session/{id}'],\n ['PUT', '/session/player/join', 'JoinSession', ['session', 'players']],\n ['DELETE', '/session/player/leave', 'LeaveSession', ['players']],\n ['GET', '/logs'],\n ['GET', '/artifacts'],\n ['GET', '/application'],\n ['POST', '/token', 'CreateTokenRequestBody', ['name', 'scopes', 'expires_after']],\n];\n\n/** Fields the docs require but the spec schema omits -> warn, don't fail. */\nconst KNOWN_SPEC_DOC_GAPS = [\n {\n schema: 'LeaveSession',\n field: 'session',\n note: 'docs/api-v2/reference/post-session-player-leave documents `session` as required; ' +\n 'the OpenAPI LeaveSession schema lists only `players` with additionalProperties:false. ' +\n 'client.mjs sends both.',\n },\n];\n\n// ------------------------------------------------------------------ indexer\nfunction indexSpec(yaml) {\n const paths = new Map(); // '/session' -> Set(['post','get'])\n const schemas = new Map(); // 'SessionRunBody' -> Set(['id','image',...])\n let section = null; // 'paths' | 'schemas' | other\n let curPath = null;\n let curSchema = null;\n let inProperties = false;\n\n for (const raw of yaml.split(/\\r?\\n/)) {\n if (!raw.trim() || raw.trimStart().startsWith('#')) continue;\n const indent = raw.match(/^ */)[0].length;\n const m = raw.match(/^\\s*([^:\\s#][^:#]*?)\\s*:/);\n if (!m) continue;\n let key = m[1].trim();\n if (key.startsWith('- ')) key = key.slice(2).trim();\n\n if (indent === 0) {\n section = key === 'paths' ? 'paths' : key === 'components' ? 'components' : 'other';\n curPath = curSchema = null;\n inProperties = false;\n continue;\n }\n\n if (section === 'paths') {\n if (indent === 2 && key.startsWith('/')) {\n curPath = key;\n paths.set(curPath, new Set());\n } else if (indent === 4 && curPath && ['get', 'post', 'put', 'delete', 'patch'].includes(key)) {\n paths.get(curPath).add(key);\n }\n } else if (section === 'components') {\n if (indent === 2) {\n section = key === 'schemas' ? 'schemas' : 'components';\n curSchema = null;\n inProperties = false;\n } else if (section === 'schemas' && indent === 4) {\n curSchema = key;\n schemas.set(curSchema, new Set());\n inProperties = false;\n } else if (section === 'schemas' && indent === 6 && curSchema) {\n inProperties = key === 'properties';\n } else if (section === 'schemas' && indent === 8 && curSchema && inProperties) {\n schemas.get(curSchema).add(key);\n }\n }\n }\n return { paths, schemas };\n}\n\n// ------------------------------------------------------------------ main\nconst failures = [];\nconst warnings = [];\n\nconst res = await fetch(SPEC_URL);\nif (!res.ok) {\n console.error(`Could not fetch spec: ${res.status} ${res.statusText}`);\n process.exit(1);\n}\nconst yaml = await res.text();\n\nconst version = yaml.match(/^\\s*version:\\s*(.+)$/m)?.[1]?.trim();\nconst title = yaml.match(/^\\s*title:\\s*(.+)$/m)?.[1]?.trim();\nconsole.log(`Spec: ${title} v${version}`);\nconsole.log(` ${SPEC_URL}\\n`);\n\nconst { paths, schemas } = indexSpec(yaml);\nconsole.log(`Indexed ${paths.size} paths, ${schemas.size} schemas.\\n`);\n\nfor (const [method, path, schema, fields] of CONTRACT) {\n const meth = method.toLowerCase();\n const methods = paths.get(path);\n if (!methods) {\n failures.push(`path missing from spec: ${path}`);\n continue;\n }\n if (!methods.has(meth)) {\n failures.push(`${method} ${path}: path exists but method ${meth} does not (has: ${[...methods].join(',')})`);\n continue;\n }\n let line = ` ok ${method.padEnd(6)} ${path}`;\n\n if (schema) {\n const props = schemas.get(schema);\n if (!props) {\n failures.push(`schema missing from spec: ${schema} (used by ${method} ${path})`);\n continue;\n }\n const missing = (fields ?? []).filter((f) => !props.has(f));\n if (missing.length) {\n const known = missing.filter((f) =>\n KNOWN_SPEC_DOC_GAPS.some((g) => g.schema === schema && g.field === f));\n const unexpected = missing.filter((f) => !known.includes(f));\n for (const f of known) {\n const gap = KNOWN_SPEC_DOC_GAPS.find((g) => g.schema === schema && g.field === f);\n warnings.push(`${schema}.${f} not in spec schema - ${gap.note}`);\n }\n if (unexpected.length) {\n failures.push(`${schema}: fields not in spec: ${unexpected.join(', ')} (spec has: ${[...props].join(', ')})`);\n }\n }\n line += ` [${schema}]`;\n }\n console.log(line);\n}\n\nif (warnings.length) {\n console.log('\\nWarnings (spec vs docs drift, handled in code):');\n for (const w of warnings) console.log(` ! ${w}`);\n}\n\nif (failures.length) {\n console.log('\\nFAILURES:');\n for (const f of failures) console.log(` x ${f}`);\n process.exit(1);\n}\n\nconsole.log('\\nAll endpoints and request fields used by this example match the live spec.');\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b0f3e16fb20cf257",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 137,
"ts": "2026-10-16T14:56:09.876Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 5680 bytes to /sandbox/repo/gameye-example/scripts/verify-spec.mjs",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b0f3e16fb20cf257",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now let me run the verifier and the dry-run demo to confirm they work.
cd /sandbox/repo/gameye-example && node scripts/verify-spec.mjs 2>&1 | tail -40
Spec: Gameye Session API v1.2.1
Spec: Gameye Session API v1.2.1
https://api.production-gameye.gameye.net/openapi.yaml
Indexed 14 paths, 0 schemas.
ok GET /region
ok GET /available-location/{image}
ok GET /tag/{region}/{image}/{version}
ok GET /session
ok GET /session/{id}
ok DELETE /session/{id}
ok GET /logs
ok GET /artifacts
ok GET /application
FAILURES:
x schema missing from spec: SessionRunBody (used by POST /session)
x schema missing from spec: JoinSession (used by PUT /session/player/join)
x schema missing from spec: LeaveSession (used by DELETE /session/player/leave)
x schema missing from spec: CreateTokenRequestBody (used by POST /token)
Recorded tool call
{
"type": "tool_call",
"seq": 140,
"ts": "2026-10-16T14:56:11.747Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && node scripts/verify-spec.mjs 2>&1 | tail -40"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a5ed5f5be79d5b96",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 143,
"ts": "2026-10-16T14:56:13.248Z",
"turn": 1,
"output": {
"content": [
{
"text": "Spec: Gameye Session API v1.2.1\n https://api.production-gameye.gameye.net/openapi.yaml\n\nIndexed 14 paths, 0 schemas.\n\n ok GET /region\n ok GET /available-location/{image}\n ok GET /tag/{region}/{image}/{version}\n ok GET /session\n ok GET /session/{id}\n ok DELETE /session/{id}\n ok GET /logs\n ok GET /artifacts\n ok GET /application\n\nFAILURES:\n x schema missing from spec: SessionRunBody (used by POST /session)\n x schema missing from spec: JoinSession (used by PUT /session/player/join)\n x schema missing from spec: LeaveSession (used by DELETE /session/player/leave)\n x schema missing from spec: CreateTokenRequestBody (used by POST /token)\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a5ed5f5be79d5b96",
"is_error": false,
"tool_kind": "shell"
}cd /sandbox/repo/gameye-example && echo "=== no image set ==="; node src/lifecycle-demo.mjs --dry-run; echo "exit=$?"
=== no image set ===
=== no image set === Gameye Session API - lifecycle example environment : sandbox image : (unset) token : (unset) mode : DRY RUN (no requests will be sent) BLOCKED: GAMEYE_IMAGE is not set. It must be the name of a Gameye application that already exists and has at least one image tag loaded on nodes (create it in the Admin Panel, or via POST /application). exit=2
Recorded tool call
{
"type": "tool_call",
"seq": 141,
"ts": "2026-10-16T14:56:11.747Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && echo \"=== no image set ===\"; node src/lifecycle-demo.mjs --dry-run; echo \"exit=$?\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-95df98837acc0ab1",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 142,
"ts": "2026-10-16T14:56:11.747Z",
"turn": 1,
"output": {
"content": [
{
"text": "=== no image set ===\nGameye Session API - lifecycle example\n environment : sandbox\n image : (unset)\n token : (unset)\n mode : DRY RUN (no requests will be sent)\n\nBLOCKED: GAMEYE_IMAGE is not set.\nIt must be the name of a Gameye application that already exists and has at\nleast one image tag loaded on nodes (create it in the Admin Panel, or via\nPOST /application).\nexit=2\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-95df98837acc0ab1",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The paths work (14 paths indexed) but schemas indexed 0 — my parser bug. The issue: components: is at indent 0, then schemas: at indent 2. My code sets section='components' at indent 0, then at indent 2 with key 'schemas' sets section='schemas'. That should work.
Wait — the problem: the regex raw.match(/^\s*([^:\s#][^:#]*?)\s*:/) — for a line like components: fine. Hmm but maybe components: appears AFTER paths and something resets. Let me debug: check indentation of components: and schemas: in the actual file.
Actually potential issue: within paths section, there might be a line at indent 0 that resets section... like components: itself. Then section='components'. Then indent 2 schemas: → section='schemas'. Then indent 4 schema names.
But maybe the spec's components: is not at indent 0? Let me check. Or maybe securitySchemes: comes first at indent 2 and my code sets section back to 'components' — that's handled (key !== 'schemas' → section='components'), then schemas: at indent 2 → 'schemas'. OK.
Let me debug by printing the actual lines.
Ah wait — I bet the issue is the key regex. For SessionRunBody: the regex ^\s*([^:\s#][^:#]*?)\s*: → captures 'SessionRunBody'. Fine.
Hmm, but note: [^:\s#] for first char. 'S' fine.
Let me actually debug. Possibly the issue: in the paths branch, once section==='schemas' we go to else if (section === 'components') — but section is now 'schemas', NOT 'components'! So the branch section === 'components' fails and nothing gets processed. That's the bug: I set section='schemas' but then the outer condition requires section==='components'.
Fix: restructure to handle section 'schemas' separately.
Let me rewrite that logic properly.
Also note: paths indexed 14 but there are 14 paths — correct.
Let me fix the parser.
Paths index correctly but schemas don't — a branch bug in my indexer (section becomes 'schemas', so the === 'components' guard never fires). Fixing:
File edits
1 file
Modifiedgameye-example/scripts/verify-spec.mjs+2−2View changes
/sandbox/repo/gameye-example/scripts/verify-spec.mjs
Replacement excerpt
- let section = null; // 'paths' | 'schemas' | other+ let section = null; // 'paths' | 'components' | 'schemas' | otherlet curPath = null;
Replacement excerpt
- } else if (section === 'components') {+ } else if (section === 'components' || section === 'schemas') {if (indent === 2) {section = key === 'schemas' ? 'schemas' : 'components';curSchema = null;inProperties = false;} else if (section === 'schemas' && indent === 4) {
Source: edit · chatcmpl-tool-8216824b94edf955
Recorded tool call
{
"type": "tool_call",
"seq": 147,
"ts": "2026-10-16T14:56:17.887Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/scripts/verify-spec.mjs",
"edits": [
{
"newText": " let section = null; // 'paths' | 'components' | 'schemas' | other\n let curPath = null;",
"oldText": " let section = null; // 'paths' | 'schemas' | other\n let curPath = null;"
},
{
"newText": " } else if (section === 'components' || section === 'schemas') {\n if (indent === 2) {\n section = key === 'schemas' ? 'schemas' : 'components';\n curSchema = null;\n inProperties = false;\n } else if (section === 'schemas' && indent === 4) {",
"oldText": " } else if (section === 'components') {\n if (indent === 2) {\n section = key === 'schemas' ? 'schemas' : 'components';\n curSchema = null;\n inProperties = false;\n } else if (section === 'schemas' && indent === 4) {"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8216824b94edf955",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 148,
"ts": "2026-10-16T14:56:17.887Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 2 block(s) in /sandbox/repo/gameye-example/scripts/verify-spec.mjs.",
"type": "text"
}
],
"details": {
"diff": " ...\n 42 // ------------------------------------------------------------------ indexer\n 43 function indexSpec(yaml) {\n 44 const paths = new Map(); // '/session' -> Set(['post','get'])\n 45 const schemas = new Map(); // 'SessionRunBody' -> Set(['id','image',...])\n- 46 let section = null; // 'paths' | 'schemas' | other\n+ 46 let section = null; // 'paths' | 'components' | 'schemas' | other\n 47 let curPath = null;\n 48 let curSchema = null;\n 49 let inProperties = false;\n 50 \n ...\n 69 paths.set(curPath, new Set());\n 70 } else if (indent === 4 && curPath && ['get', 'post', 'put', 'delete', 'patch'].includes(key)) {\n 71 paths.get(curPath).add(key);\n 72 }\n- 73 } else if (section === 'components') {\n+ 73 } else if (section === 'components' || section === 'schemas') {\n 74 if (indent === 2) {\n 75 section = key === 'schemas' ? 'schemas' : 'components';\n 76 curSchema = null;\n 77 inProperties = false;\n ...",
"patch": "--- /sandbox/repo/gameye-example/scripts/verify-spec.mjs\n+++ /sandbox/repo/gameye-example/scripts/verify-spec.mjs\n@@ -42,9 +42,9 @@\n // ------------------------------------------------------------------ indexer\n function indexSpec(yaml) {\n const paths = new Map(); // '/session' -> Set(['post','get'])\n const schemas = new Map(); // 'SessionRunBody' -> Set(['id','image',...])\n- let section = null; // 'paths' | 'schemas' | other\n+ let section = null; // 'paths' | 'components' | 'schemas' | other\n let curPath = null;\n let curSchema = null;\n let inProperties = false;\n \n@@ -69,9 +69,9 @@\n paths.set(curPath, new Set());\n } else if (indent === 4 && curPath && ['get', 'post', 'put', 'delete', 'patch'].includes(key)) {\n paths.get(curPath).add(key);\n }\n- } else if (section === 'components') {\n+ } else if (section === 'components' || section === 'schemas') {\n if (indent === 2) {\n section = key === 'schemas' ? 'schemas' : 'components';\n curSchema = null;\n inProperties = false;\n",
"firstChangedLine": 46
}
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8216824b94edf955",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Wait, there's still an issue: if (indent === 0) sets section based on key. components: → section='components'. Good.
But there's a subtlety: at indent 2 inside components, if key is 'securitySchemes' → section='components'. Then when schemas: appears at indent 2 → section='schemas'. Good.
But if within 'schemas', a nested key appears at indent 2? Unlikely.
Let me re-run.
cd /sandbox/repo/gameye-example && node scripts/verify-spec.mjs 2>&1 | tail -30
Spec: Gameye Session API v1.2.1
Spec: Gameye Session API v1.2.1
https://api.production-gameye.gameye.net/openapi.yaml
Indexed 14 paths, 31 schemas.
ok GET /region
ok GET /available-location/{image}
ok GET /tag/{region}/{image}/{version}
ok POST /session [SessionRunBody]
ok GET /session
ok GET /session/{id}
ok DELETE /session/{id}
ok PUT /session/player/join [JoinSession]
ok DELETE /session/player/leave [LeaveSession]
ok GET /logs
ok GET /artifacts
ok GET /application
ok POST /token [CreateTokenRequestBody]
All endpoints and request fields used by this example match the live spec.
Recorded tool call
{
"type": "tool_call",
"seq": 151,
"ts": "2026-10-16T14:56:19.176Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && node scripts/verify-spec.mjs 2>&1 | tail -30"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bf20aec53dfafe7e",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 152,
"ts": "2026-10-16T14:56:20.474Z",
"turn": 1,
"output": {
"content": [
{
"text": "Spec: Gameye Session API v1.2.1\n https://api.production-gameye.gameye.net/openapi.yaml\n\nIndexed 14 paths, 31 schemas.\n\n ok GET /region\n ok GET /available-location/{image}\n ok GET /tag/{region}/{image}/{version}\n ok POST /session [SessionRunBody]\n ok GET /session\n ok GET /session/{id}\n ok DELETE /session/{id}\n ok PUT /session/player/join [JoinSession]\n ok DELETE /session/player/leave [LeaveSession]\n ok GET /logs\n ok GET /artifacts\n ok GET /application\n ok POST /token [CreateTokenRequestBody]\n\nAll endpoints and request fields used by this example match the live spec.\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bf20aec53dfafe7e",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
All 13 endpoints verified against the live spec. But the LeaveSession warning didn't show! Because my check for session field: KNOWN_SPEC_DOC_GAPS says LeaveSession.session should be missing from spec. The contract for LeaveSession only lists ['players'], so 'session' was never checked. I need to include 'session' in the contract fields for LeaveSession so the known-gap warning triggers.
Let me update the CONTRACT to include 'session' for LeaveSession so the warning fires and documents the drift.
Let me do that.
Then verify warning appears.
Then run the dry-run demo with an image set to show the plan.
Then also test the real API call path without a token to show the error handling works (it will hit 401). Actually I can test with a bogus token to exercise the error handler end-to-end — that's a real request against the live API, light and safe. Let me do that to prove error handling works.
Let me make these changes.
All 13 endpoints verified. Let me make the known spec/docs drift actually surface (it wasn't being checked):
File edits
1 file
Modifiedgameye-example/scripts/verify-spec.mjs+3−1View changes
/sandbox/repo/gameye-example/scripts/verify-spec.mjs
Replacement excerpt
- ['DELETE', '/session/player/leave', 'LeaveSession', ['players']],+ // `session` is intentionally listed: it is documented as required but absent from+ // the spec schema, so it should surface as a known-drift warning (not a failure).+ ['DELETE', '/session/player/leave', 'LeaveSession', ['players', 'session']],
Source: edit · chatcmpl-tool-bad0046cd184d23e
Recorded tool call
{
"type": "tool_call",
"seq": 156,
"ts": "2026-10-16T14:56:23.497Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/scripts/verify-spec.mjs",
"edits": [
{
"newText": " // `session` is intentionally listed: it is documented as required but absent from\n // the spec schema, so it should surface as a known-drift warning (not a failure).\n ['DELETE', '/session/player/leave', 'LeaveSession', ['players', 'session']],",
"oldText": " ['DELETE', '/session/player/leave', 'LeaveSession', ['players']],"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bad0046cd184d23e",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 157,
"ts": "2026-10-16T14:56:23.497Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 1 block(s) in /sandbox/repo/gameye-example/scripts/verify-spec.mjs.",
"type": "text"
}
],
"details": {
"diff": " ...\n 20 ['GET', '/session'],\n 21 ['GET', '/session/{id}'],\n 22 ['DELETE', '/session/{id}'],\n 23 ['PUT', '/session/player/join', 'JoinSession', ['session', 'players']],\n- 24 ['DELETE', '/session/player/leave', 'LeaveSession', ['players']],\n+ 24 // `session` is intentionally listed: it is documented as required but absent from\n+ 25 // the spec schema, so it should surface as a known-drift warning (not a failure).\n+ 26 ['DELETE', '/session/player/leave', 'LeaveSession', ['players', 'session']],\n 25 ['GET', '/logs'],\n 26 ['GET', '/artifacts'],\n 27 ['GET', '/application'],\n 28 ['POST', '/token', 'CreateTokenRequestBody', ['name', 'scopes', 'expires_after']],\n ...",
"patch": "--- /sandbox/repo/gameye-example/scripts/verify-spec.mjs\n+++ /sandbox/repo/gameye-example/scripts/verify-spec.mjs\n@@ -20,9 +20,11 @@\n ['GET', '/session'],\n ['GET', '/session/{id}'],\n ['DELETE', '/session/{id}'],\n ['PUT', '/session/player/join', 'JoinSession', ['session', 'players']],\n- ['DELETE', '/session/player/leave', 'LeaveSession', ['players']],\n+ // `session` is intentionally listed: it is documented as required but absent from\n+ // the spec schema, so it should surface as a known-drift warning (not a failure).\n+ ['DELETE', '/session/player/leave', 'LeaveSession', ['players', 'session']],\n ['GET', '/logs'],\n ['GET', '/artifacts'],\n ['GET', '/application'],\n ['POST', '/token', 'CreateTokenRequestBody', ['name', 'scopes', 'expires_after']],\n",
"firstChangedLine": 24
}
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bad0046cd184d23e",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Wait — the logic: missing = fields not in props. known = missing that are in KNOWN_SPEC_DOC_GAPS. unexpected = missing not known. For LeaveSession, 'session' is missing from props → known includes 'session' → warning added. Good.
Let me run it.
Also run the dry-run with an image set, and test the bogus-token error path.
cd /sandbox/repo/gameye-example && node scripts/verify-spec.mjs 2>&1 | tail -8; echo "exit=$?"
ok GET /artifacts
ok GET /artifacts ok GET /application ok POST /token [CreateTokenRequestBody] Warnings (spec vs docs drift, handled in code): ! LeaveSession.session not in spec schema - docs/api-v2/reference/post-session-player-leave documents `session` as required; the OpenAPI LeaveSession schema lists only `players` with additionalProperties:false. client.mjs sends both. All endpoints and request fields used by this example match the live spec. exit=0
Recorded tool call
{
"type": "tool_call",
"seq": 160,
"ts": "2026-10-16T14:56:25.251Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && node scripts/verify-spec.mjs 2>&1 | tail -8; echo \"exit=$?\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-99046fc0563c4f73",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 163,
"ts": "2026-10-16T14:56:26.912Z",
"turn": 1,
"output": {
"content": [
{
"text": " ok GET /artifacts\n ok GET /application\n ok POST /token [CreateTokenRequestBody]\n\nWarnings (spec vs docs drift, handled in code):\n ! LeaveSession.session not in spec schema - docs/api-v2/reference/post-session-player-leave documents `session` as required; the OpenAPI LeaveSession schema lists only `players` with additionalProperties:false. client.mjs sends both.\n\nAll endpoints and request fields used by this example match the live spec.\nexit=0\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-99046fc0563c4f73",
"is_error": false,
"tool_kind": "shell"
}cd /sandbox/repo/gameye-example && GAMEYE_IMAGE=my-game GAMEYE_ENV=sandbox node src/lifecycle-demo.mjs --dry-run; echo "exit=$?"
Gameye Session API - lifecycle example
Gameye Session API - lifecycle example
environment : sandbox
image : my-game
token : (unset)
mode : DRY RUN (no requests will be sent)
Requests that would be sent to https://api.sandbox-gameye.gameye.net:
GET https://api.sandbox-gameye.gameye.net/region
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: regions:read)
GET https://api.sandbox-gameye.gameye.net/available-location/my-game
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: regions:read)
POST https://api.sandbox-gameye.gameye.net/session
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:start)
body: {"id":"<session-uuid>","image":"my-game","location":"<first-available-location>","ttl":"10m","args":["--max-players=8"],"env":{"MATCH_MODE":"ranked"},"labels":{"example":"lifecycle-demo"}}
GET https://api.sandbox-gameye.gameye.net/session/<session-uuid>
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:read)
PUT https://api.sandbox-gameye.gameye.net/session/player/join
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:read)
body: {"session":"<session-uuid>","players":["player-1","player-2"]}
GET https://api.sandbox-gameye.gameye.net/logs?id=<session-uuid>&follow=false
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: logs:read)
DELETE https://api.sandbox-gameye.gameye.net/session/player/leave
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:read)
body: {"session":"<session-uuid>","players":["player-1","player-2"]}
DELETE https://api.sandbox-gameye.gameye.net/session/<session-uuid>
Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:stop)
No requests sent. Set GAMEYE_API_TOKEN and drop --dry-run to execute.
exit=0
Recorded tool call
{
"type": "tool_call",
"seq": 161,
"ts": "2026-10-16T14:56:25.251Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && GAMEYE_IMAGE=my-game GAMEYE_ENV=sandbox node src/lifecycle-demo.mjs --dry-run; echo \"exit=$?\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a36e8c5294d668b4",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 162,
"ts": "2026-10-16T14:56:25.251Z",
"turn": 1,
"output": {
"content": [
{
"text": "Gameye Session API - lifecycle example\n environment : sandbox\n image : my-game\n token : (unset)\n mode : DRY RUN (no requests will be sent)\n\nRequests that would be sent to https://api.sandbox-gameye.gameye.net:\n\nGET https://api.sandbox-gameye.gameye.net/region\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: regions:read)\nGET https://api.sandbox-gameye.gameye.net/available-location/my-game\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: regions:read)\nPOST https://api.sandbox-gameye.gameye.net/session\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:start)\n body: {\"id\":\"<session-uuid>\",\"image\":\"my-game\",\"location\":\"<first-available-location>\",\"ttl\":\"10m\",\"args\":[\"--max-players=8\"],\"env\":{\"MATCH_MODE\":\"ranked\"},\"labels\":{\"example\":\"lifecycle-demo\"}}\nGET https://api.sandbox-gameye.gameye.net/session/<session-uuid>\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:read)\nPUT https://api.sandbox-gameye.gameye.net/session/player/join\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:read)\n body: {\"session\":\"<session-uuid>\",\"players\":[\"player-1\",\"player-2\"]}\nGET https://api.sandbox-gameye.gameye.net/logs?id=<session-uuid>&follow=false\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: logs:read)\nDELETE https://api.sandbox-gameye.gameye.net/session/player/leave\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:read)\n body: {\"session\":\"<session-uuid>\",\"players\":[\"player-1\",\"player-2\"]}\nDELETE https://api.sandbox-gameye.gameye.net/session/<session-uuid>\n Authorization: Bearer <GAMEYE_API_TOKEN> (scope: session:stop)\n\nNo requests sent. Set GAMEYE_API_TOKEN and drop --dry-run to execute.\nexit=0\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-a36e8c5294d668b4",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Both work. Now let me test the real request path end-to-end with a bogus token, to prove the client + error handling works against the live API (it will 401 at step 1, which is the honest blocker).
This is a single light GET request — safe.
Now let me exercise the real request path against the live API (with a bogus token, so it stops at the first call):
cd /sandbox/repo/gameye-example && GAMEYE_IMAGE=my-game GAMEYE_ENV=sandbox GAMEYE_API_TOKEN=not-a-real-token timeout 40 node src/lifecycle-demo.mjs; echo "exit=$?"
Gameye Session API - lifecycle example
Gameye Session API - lifecycle example environment : sandbox image : my-game token : not-a-... (16 chars) base url : https://api.sandbox-gameye.gameye.net [1] GET /region API ERROR 401 UNAUTHORIZED GET /region -> 401 UNAUTHORIZED: Please verify that you are using a valid auth token. identifier (give to support): 9b8c474f-d8c2-4d92-90e3-d86c4b2d760a hint: Token missing/mistyped, or not sent as "Authorization: Bearer <token>". exit=1
Recorded tool call
{
"type": "tool_call",
"seq": 167,
"ts": "2026-10-16T14:56:28.558Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && GAMEYE_IMAGE=my-game GAMEYE_ENV=sandbox GAMEYE_API_TOKEN=not-a-real-token timeout 40 node src/lifecycle-demo.mjs; echo \"exit=$?\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8925df2754e74ce2",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 168,
"ts": "2026-10-16T14:56:29.339Z",
"turn": 1,
"output": {
"content": [
{
"text": "Gameye Session API - lifecycle example\n environment : sandbox\n image : my-game\n token : not-a-... (16 chars)\n base url : https://api.sandbox-gameye.gameye.net\n\n[1] GET /region\n\nAPI ERROR 401 UNAUTHORIZED\n GET /region -> 401 UNAUTHORIZED: Please verify that you are using a valid auth token.\n identifier (give to support): 9b8c474f-d8c2-4d92-90e3-d86c4b2d760a\n hint: Token missing/mistyped, or not sent as \"Authorization: Bearer <token>\".\nexit=1\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8925df2754e74ce2",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The client works end-to-end against the live API: it made a real request, received a real Gameye error envelope, parsed code/details/identifier, and mapped a hint. The only thing missing is a valid token.
Now write the README with:
- What this is
- Quickstart
- Pricing explanation
- The blocker
- The spec/docs drift note
- Reference Dockerfile? I'll include a short note about the prerequisite (application + tag) rather than a full Dockerfile, or include a reference Dockerfile since it's genuinely part of quickstart. Let me include a minimal reference Dockerfile since it's part of the real workflow and costs nothing — but clearly marked "not built here".
Let me write the README and add the Dockerfile reference.
Then final: file listing, and answer the three questions:
- How pricing works
- Whether this product will be easy for me to manage
- Blocked? Yes — need API token.
Let me write README.md.
Pricing summary from the pricing page (updated Oct 16, 2026):
- Billed per vCPU per hour, per-second billing, no minimum session length
- On-demand: $0.07/vCPU/hr (1 vCPU : 2 GB RAM), $0.10/vCPU/hr (1 vCPU : 4 GB RAM)
- Reserved: $0.035/vCPU/hr (1:2), $0.042/vCPU/hr (1:4) — min commitment one 8-core machine per region, guaranteed AMD Zen 4/Zen 5
- Zero egress/bandwidth fees — included
- TTL: billing stops at termination; empty TTL = runs until stopped
- 99.7% contractual SLA
- Bring Your Own Infrastructure: flat monthly fee per vCPU, from $2/vCPU/month, priced on provisioned vCPU count
- Free trial / self-serve signup; early-stage studios can use platform free of charge (no dynamic scaling in that phase)
- No SDK required in your game server (pure REST)
- 402 Payment Required when quota exhausted
Let me write the README.
The client works end-to-end against the live API. Let me finish the docs and a reference Dockerfile.
File edits
1 file
Writtengameye-example/README.mdView changes
/sandbox/repo/gameye-example/README.md
Written content · previous contents were not recorded
# Gameye Session API — minimal exampleA dependency-free Node.js example that drives the **full Gameye session lifecycle**against the hosted Gameye Session API. No local services, no build step, no`node_modules` — it uses the global `fetch` in Node 18+.Every endpoint and request field used here is checked against Gameye's **live**OpenAPI spec (`Gameye Session API v1.2.1`) by `npm run verify-spec`.```gameye-example/├── package.json├── .env.example├── src/│ ├── client.mjs # thin client: one method per API endpoint│ └── lifecycle-demo.mjs # runnable end-to-end example├── scripts/│ └── verify-spec.mjs # validates the example against the live OpenAPI spec└── reference/└── Dockerfile # example game-server image (NOT built by this repo)```## What Gameye isManaged container orchestration for multiplayer game servers. You push a Dockerimage of your game server to a registry, register it as a Gameye *application*,then call `POST /session` with a region. Gameye returns an IP and ports; yourplayers connect. You stop it with `DELETE /session/{id}`, or set a `ttl` and letGameye terminate it.There is **no SDK to embed in your game server** — it is a plain REST API, so yourmatchmaker/backend is the only integration point.## Quickstart```bashcp .env.example .env # fill in GAMEYE_API_TOKEN and GAMEYE_IMAGEnpm run demo # real runnpm run demo:dry # print the exact HTTP calls, send nonenpm run verify-spec # check the example against the live spec```### Prerequisites (done once, in the Gameye Admin Panel)1. **Push a game-server image** to Docker Hub or GHCR — see `reference/Dockerfile`.2. **Create an application** in the Admin Panel (or `POST /application`) pointing atthat registry/repository, with its resource profile, ports and regions.3. **Enable an image tag** so it is loaded onto nodes (Applications → Tags).4. **Create an API token** (Users → account → Edit → API Tokens → + Add Token).The full token is shown **only once**, at creation time.Without step 3 the API has nowhere to schedule your session:`GET /available-location/{image}` comes back empty and `POST /session` 404s.### Scopes this example needs| Scope | Used for || --- | --- || `regions:read` | `GET /region`, `GET /available-location/{image}` || `session:start` | `POST /session` || `session:read` | `GET /session`, `GET /session/{id}`, player join/leave || `session:stop` | `DELETE /session/{id}` || `logs:read` | `GET /logs` |## The lifecycle this example runs| # | Call | Purpose || --- | --- | --- || 1 | `GET /region` | Regions Gameye can schedule into || 2 | `GET /available-location/{image}` | Regions that can run *your* image right now || 3 | `POST /session` | Start a session (`ttl` always set as a safety net) || 4 | `GET /session/{id}` | Inspect it → `host` + `ports` players connect to || 5 | `PUT /session/player/join` | Register player ids || 6 | `GET /logs?id=…&follow=false` | Snapshot of the server's stdout/stderr || 7 | `DELETE /session/player/leave` | Players leave || 8 | `DELETE /session/{id}` | Stop it → `204`; billing stops at termination |Step 8 runs in a `finally`, so a failure in steps 4–7 still stops the session.`POST /session` returns:```json{"id": "f7965a22-e327-4541-a30e-8262890d3a29","host": "203.0.113.42","ports": [{ "type": "udp", "container": 7777, "host": 49152 }]}```Players connect to `host` on `ports[].host` (`203.0.113.42:49152`). Inside thecontainer that same port is exposed as `GAMEYE_PORT_UDP_7777`.## Notes worth knowing- **Environments.** `sandbox` (default here) and `production`:`https://api.{sandbox,production}-gameye.gameye.net`. Test in sandbox.- **Always set `ttl`.** An empty TTL means the session runs until explicitlystopped. TTL expiry is an *immediate force-terminate* with no graceful shutdownsignal — pull artifacts (`GET /artifacts`) or write state externally beforehand.- **Warm pools change semantics.** If a warm pool is configured for theregion+image, a pre-started session is claimed and your `args`, `env`, `labels`,`version` and `restart` are **ignored** (warm-pool sessions use a static config).- **Delegated tokens.** `POST /token` mints short-lived scoped tokens(`expires_after: "2h"`, units `m`/`h`). A token can only delegate scopes italready holds.- **Spec vs docs drift.** `DELETE /session/player/leave`: the reference docs list`session` **and** `players` as required, but the published OpenAPI `LeaveSession`schema declares only `players` with `additionalProperties: false`. `client.mjs`sends both, and `verify-spec` reports this as a warning rather than failing.- **Error envelope.** Non-2xx responses are JSON with `code`, `message`, `details`,`identifier` (quote this to support) and `violations`. `GameyeApiError` surfacesall of them; the demo maps common statuses to hints:| Status | Meaning || --- | --- || 401 | Token missing/mistyped, or not sent as `Bearer <token>` || 402 | Quota exhausted || 403 | Token lacks the required scope || 404 | Region/application/tag or session id does not exist || 409 | Session id already in use — use a fresh UUID || 420 | Cannot start in that region now (capacity/rate limit) — retry || 422 | Invalid request syntax |## How pricing worksPriced **per vCPU per hour, billed per second**, with no minimum session length and**no egress/bandwidth charges** (all bandwidth is included). Rates depend on theRAM ratio of your container profile:| Mode | 1 vCPU : 2 GB RAM | 1 vCPU : 4 GB RAM | Notes || --- | --- | --- | --- || **On demand** | $0.07 /vCPU/hr | $0.10 /vCPU/hr | No commitment; scales to zero instantly || **Reserved** | $0.035 /vCPU/hr | $0.042 /vCPU/hr | Guaranteed AMD Zen 4/Zen 5 at no extra cost; minimum one 8-core machine per region |So cost is driven by **container size × wall-clock seconds alive**, not by sessioncount, players, or bandwidth. Practical consequences:- Billing stops the moment the container terminates — via `DELETE /session/{id}` orTTL expiry, whichever comes first. A session you forget to stop bills until itsTTL fires, which is why the example always sets one.- Typical mix: reserved capacity for your baseline load, on-demand for overflow.- **Bring Your Own Infrastructure** is a different model: a flat monthly fee perprovisioned vCPU (from $2/vCPU/month), no per-session or egress charges, and youpay your own cloud/bare-metal provider directly.- Contractual 99.7% SLA; platform features (orchestration API, warm pools, livemetrics/logs, Admin Panel, matchmaker integrations) are included at no extra charge.- Not free in general, but there is a self-serve free trial, and studios early indevelopment can use the platform free of charge (without dynamic scaling in thatphase). Hitting your credit limit returns `402`.Figures above are from the Gameye pricing page (updated 2026-10-16); confirm currentrates and your container profile's RAM ratio before modelling costs.
Source: write · chatcmpl-tool-b96d6a1e39660be8
Recorded tool call
{
"type": "tool_call",
"seq": 172,
"ts": "2026-10-16T14:56:41.917Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/README.md",
"content": "# Gameye Session API — minimal example\n\nA dependency-free Node.js example that drives the **full Gameye session lifecycle**\nagainst the hosted Gameye Session API. No local services, no build step, no\n`node_modules` — it uses the global `fetch` in Node 18+.\n\nEvery endpoint and request field used here is checked against Gameye's **live**\nOpenAPI spec (`Gameye Session API v1.2.1`) by `npm run verify-spec`.\n\n```\ngameye-example/\n├── package.json\n├── .env.example\n├── src/\n│ ├── client.mjs # thin client: one method per API endpoint\n│ └── lifecycle-demo.mjs # runnable end-to-end example\n├── scripts/\n│ └── verify-spec.mjs # validates the example against the live OpenAPI spec\n└── reference/\n └── Dockerfile # example game-server image (NOT built by this repo)\n```\n\n## What Gameye is\n\nManaged container orchestration for multiplayer game servers. You push a Docker\nimage of your game server to a registry, register it as a Gameye *application*,\nthen call `POST /session` with a region. Gameye returns an IP and ports; your\nplayers connect. You stop it with `DELETE /session/{id}`, or set a `ttl` and let\nGameye terminate it.\n\nThere is **no SDK to embed in your game server** — it is a plain REST API, so your\nmatchmaker/backend is the only integration point.\n\n## Quickstart\n\n```bash\ncp .env.example .env # fill in GAMEYE_API_TOKEN and GAMEYE_IMAGE\nnpm run demo # real run\nnpm run demo:dry # print the exact HTTP calls, send none\nnpm run verify-spec # check the example against the live spec\n```\n\n### Prerequisites (done once, in the Gameye Admin Panel)\n\n1. **Push a game-server image** to Docker Hub or GHCR — see `reference/Dockerfile`.\n2. **Create an application** in the Admin Panel (or `POST /application`) pointing at\n that registry/repository, with its resource profile, ports and regions.\n3. **Enable an image tag** so it is loaded onto nodes (Applications → Tags).\n4. **Create an API token** (Users → account → Edit → API Tokens → + Add Token).\n The full token is shown **only once**, at creation time.\n\nWithout step 3 the API has nowhere to schedule your session:\n`GET /available-location/{image}` comes back empty and `POST /session` 404s.\n\n### Scopes this example needs\n\n| Scope | Used for |\n| --- | --- |\n| `regions:read` | `GET /region`, `GET /available-location/{image}` |\n| `session:start` | `POST /session` |\n| `session:read` | `GET /session`, `GET /session/{id}`, player join/leave |\n| `session:stop` | `DELETE /session/{id}` |\n| `logs:read` | `GET /logs` |\n\n## The lifecycle this example runs\n\n| # | Call | Purpose |\n| --- | --- | --- |\n| 1 | `GET /region` | Regions Gameye can schedule into |\n| 2 | `GET /available-location/{image}` | Regions that can run *your* image right now |\n| 3 | `POST /session` | Start a session (`ttl` always set as a safety net) |\n| 4 | `GET /session/{id}` | Inspect it → `host` + `ports` players connect to |\n| 5 | `PUT /session/player/join` | Register player ids |\n| 6 | `GET /logs?id=…&follow=false` | Snapshot of the server's stdout/stderr |\n| 7 | `DELETE /session/player/leave` | Players leave |\n| 8 | `DELETE /session/{id}` | Stop it → `204`; billing stops at termination |\n\nStep 8 runs in a `finally`, so a failure in steps 4–7 still stops the session.\n\n`POST /session` returns:\n\n```json\n{\n \"id\": \"f7965a22-e327-4541-a30e-8262890d3a29\",\n \"host\": \"203.0.113.42\",\n \"ports\": [{ \"type\": \"udp\", \"container\": 7777, \"host\": 49152 }]\n}\n```\n\nPlayers connect to `host` on `ports[].host` (`203.0.113.42:49152`). Inside the\ncontainer that same port is exposed as `GAMEYE_PORT_UDP_7777`.\n\n## Notes worth knowing\n\n- **Environments.** `sandbox` (default here) and `production`:\n `https://api.{sandbox,production}-gameye.gameye.net`. Test in sandbox.\n- **Always set `ttl`.** An empty TTL means the session runs until explicitly\n stopped. TTL expiry is an *immediate force-terminate* with no graceful shutdown\n signal — pull artifacts (`GET /artifacts`) or write state externally beforehand.\n- **Warm pools change semantics.** If a warm pool is configured for the\n region+image, a pre-started session is claimed and your `args`, `env`, `labels`,\n `version` and `restart` are **ignored** (warm-pool sessions use a static config).\n- **Delegated tokens.** `POST /token` mints short-lived scoped tokens\n (`expires_after: \"2h\"`, units `m`/`h`). A token can only delegate scopes it\n already holds.\n- **Spec vs docs drift.** `DELETE /session/player/leave`: the reference docs list\n `session` **and** `players` as required, but the published OpenAPI `LeaveSession`\n schema declares only `players` with `additionalProperties: false`. `client.mjs`\n sends both, and `verify-spec` reports this as a warning rather than failing.\n- **Error envelope.** Non-2xx responses are JSON with `code`, `message`, `details`,\n `identifier` (quote this to support) and `violations`. `GameyeApiError` surfaces\n all of them; the demo maps common statuses to hints:\n\n | Status | Meaning |\n | --- | --- |\n | 401 | Token missing/mistyped, or not sent as `Bearer <token>` |\n | 402 | Quota exhausted |\n | 403 | Token lacks the required scope |\n | 404 | Region/application/tag or session id does not exist |\n | 409 | Session id already in use — use a fresh UUID |\n | 420 | Cannot start in that region now (capacity/rate limit) — retry |\n | 422 | Invalid request syntax |\n\n## How pricing works\n\nPriced **per vCPU per hour, billed per second**, with no minimum session length and\n**no egress/bandwidth charges** (all bandwidth is included). Rates depend on the\nRAM ratio of your container profile:\n\n| Mode | 1 vCPU : 2 GB RAM | 1 vCPU : 4 GB RAM | Notes |\n| --- | --- | --- | --- |\n| **On demand** | $0.07 /vCPU/hr | $0.10 /vCPU/hr | No commitment; scales to zero instantly |\n| **Reserved** | $0.035 /vCPU/hr | $0.042 /vCPU/hr | Guaranteed AMD Zen 4/Zen 5 at no extra cost; minimum one 8-core machine per region |\n\nSo cost is driven by **container size × wall-clock seconds alive**, not by session\ncount, players, or bandwidth. Practical consequences:\n\n- Billing stops the moment the container terminates — via `DELETE /session/{id}` or\n TTL expiry, whichever comes first. A session you forget to stop bills until its\n TTL fires, which is why the example always sets one.\n- Typical mix: reserved capacity for your baseline load, on-demand for overflow.\n- **Bring Your Own Infrastructure** is a different model: a flat monthly fee per\n provisioned vCPU (from $2/vCPU/month), no per-session or egress charges, and you\n pay your own cloud/bare-metal provider directly.\n- Contractual 99.7% SLA; platform features (orchestration API, warm pools, live\n metrics/logs, Admin Panel, matchmaker integrations) are included at no extra charge.\n- Not free in general, but there is a self-serve free trial, and studios early in\n development can use the platform free of charge (without dynamic scaling in that\n phase). Hitting your credit limit returns `402`.\n\nFigures above are from the Gameye pricing page (updated 2026-10-16); confirm current\nrates and your container profile's RAM ratio before modelling costs.\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b96d6a1e39660be8",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 173,
"ts": "2026-10-16T14:56:41.917Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 7106 bytes to /sandbox/repo/gameye-example/README.md",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b96d6a1e39660be8",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now the reference Dockerfile. Let me check the docs for the Dockerfile page to align with what Gameye expects (ports, GAMEYE_ env vars). Let me fetch getting-started/2-docker-file quickly.
Actually let me fetch it to be accurate rather than guessing.
cd /tmp && timeout 30 curl -sSL "https://gameye.com/docs/getting-started/2-docker-file/" -o d_docker.html -w "%{http_code}\n" && python3 -c "
import re,html
t=open('d_docker.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)
m=re.search(r'<main.*?</main>',t,flags=re.S|re.I)
if m: t=m.group(0)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t); t=re.sub(r'\n\s*\n+','\n',t)
print(t.strip()[:2600])
"200
200
2. Create and push your Dockerfile 1. Create your Dockerfile Section titled “1. Create your Dockerfile”
Your image must contain everything the server needs at runtime: the server build, third-party libraries (anti-cheat, Steam SDK and so on) and an ENTRYPOINT that starts the server process. An invalid entrypoint stops the container from starting, so test it locally first.
A minimal template:
FROM ubuntu:24.04
# add a non-root user to run the server RUN useradd -m gameuser
# copy your server files, owned by that user COPY --chown=gameuser:gameuser . /home/gameuser/MyGame/ RUN chmod +x /home/gameuser/MyGame/GameServer.sh
USER gameuser
# start the game server ENTRYPOINT [ "/home/gameuser/MyGame/GameServer.sh" , "-log" ]
Gameye nodes run linux/amd64 . If you build on an Apple silicon Mac or another ARM machine, build for that platform explicitly:
Terminal window docker build --platform linux/amd64 -t mygame/server:build-001 .
2. Choose your network mode Section titled “2. Choose your network mode”
You pick the network mode when you create the application. It decides how your server finds the port players connect to.
Bridge Host How ports work Your server listens on a fixed container port (for example 7777). Gameye maps it to a free port on the node. The container uses the node’s network directly. Your server must listen on the port Gameye assigns. Port conflicts None: each container has its own network namespace Your server must use the assigned port, or it can collide with other containers What your server must do Nothing: listen on the container port you configured Read its port from an environment variable at startup Overhead Small overhead from Docker’s port mapping None
If you’re unsure, use bridge . You can change it later by editing the application.
In both modes you declare your ports on the application (protocol and container port, for example UDP 7777). You don’t need EXPOSE lines in the Dockerfile: Gameye publishes the ports configured on the application.
Port environment variables Section titled “Port environment variables”
Gameye passes every assigned host port to the container as an environment variable, in both modes:
GAMEYE_PORT_{PROTOCOL}_{CONTAINER_PORT}
Variable Meaning GAMEYE_PORT_UDP_7777 The host port players use for container port UDP 7777 GAMEYE_PORT_TCP_8080 The host port players use for container port TCP 8080
In host mode your server must listen on these ports. In bridge mode they tell the server which public port players see, which is useful if it advertises its own address. Example entrypoint
Recorded tool call
{
"type": "tool_call",
"seq": 176,
"ts": "2026-10-16T14:56:44.229Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 30 curl -sSL \"https://gameye.com/docs/getting-started/2-docker-file/\" -o d_docker.html -w \"%{http_code}\\n\" && python3 -c \"\nimport re,html\nt=open('d_docker.html',encoding='utf-8',errors='ignore').read()\nt=re.sub(r'<script.*?</script>','',t,flags=re.S|re.I); t=re.sub(r'<style.*?</style>','',t,flags=re.S|re.I)\nm=re.search(r'<main.*?</main>',t,flags=re.S|re.I)\nif m: t=m.group(0)\nt=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t); t=re.sub(r'\\n\\s*\\n+','\\n',t)\nprint(t.strip()[:2600])\n\""
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8e389502f1617660",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 177,
"ts": "2026-10-16T14:56:45.530Z",
"turn": 1,
"output": {
"content": [
{
"text": "200\n2. Create and push your Dockerfile 1. Create your Dockerfile Section titled “1. Create your Dockerfile” \n Your image must contain everything the server needs at runtime: the server build, third-party libraries (anti-cheat, Steam SDK and so on) and an ENTRYPOINT that starts the server process. An invalid entrypoint stops the container from starting, so test it locally first. \n A minimal template: \n FROM ubuntu:24.04 \n # add a non-root user to run the server RUN useradd -m gameuser \n # copy your server files, owned by that user COPY --chown=gameuser:gameuser . /home/gameuser/MyGame/ RUN chmod +x /home/gameuser/MyGame/GameServer.sh \n USER gameuser \n # start the game server ENTRYPOINT [ \"/home/gameuser/MyGame/GameServer.sh\" , \"-log\" ] \n Gameye nodes run linux/amd64 . If you build on an Apple silicon Mac or another ARM machine, build for that platform explicitly: \n Terminal window docker build --platform linux/amd64 -t mygame/server:build-001 . \n 2. Choose your network mode Section titled “2. Choose your network mode” \n You pick the network mode when you create the application. It decides how your server finds the port players connect to. \n Bridge Host How ports work Your server listens on a fixed container port (for example 7777). Gameye maps it to a free port on the node. The container uses the node’s network directly. Your server must listen on the port Gameye assigns. Port conflicts None: each container has its own network namespace Your server must use the assigned port, or it can collide with other containers What your server must do Nothing: listen on the container port you configured Read its port from an environment variable at startup Overhead Small overhead from Docker’s port mapping None \n If you’re unsure, use bridge . You can change it later by editing the application. \n In both modes you declare your ports on the application (protocol and container port, for example UDP 7777). You don’t need EXPOSE lines in the Dockerfile: Gameye publishes the ports configured on the application. \n Port environment variables Section titled “Port environment variables” \n Gameye passes every assigned host port to the container as an environment variable, in both modes: \n GAMEYE_PORT_{PROTOCOL}_{CONTAINER_PORT} \n Variable Meaning GAMEYE_PORT_UDP_7777 The host port players use for container port UDP 7777 GAMEYE_PORT_TCP_8080 The host port players use for container port TCP 8080 \n In host mode your server must listen on these ports. In bridge mode they tell the server which public port players see, which is useful if it advertises its own address. Example entrypoint\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8e389502f1617660",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Now let me write the reference Dockerfile matching the official template.
File edits
1 file
Writtengameye-example/reference/DockerfileView changes
/sandbox/repo/gameye-example/reference/Dockerfile
Written content · previous contents were not recorded
# Reference only — this repo does NOT build or push images.## This mirrors Gameye's minimal game-server template# (gameye.com/docs/getting-started/2-docker-file/). Build it yourself and push it# to Docker Hub or GHCR, then register it as a Gameye application.## docker build --platform linux/amd64 -t <registry>/<you>/my-game:build-001 .# docker push <registry>/<you>/my-game:build-001## Notes:# * Gameye nodes run linux/amd64 — always build for that platform explicitly.# * The image must contain everything needed at runtime (server build plus any# third-party libs such as anti-cheat or the Steam SDK).# * No EXPOSE lines needed: Gameye publishes the ports declared on the# application (e.g. UDP 7777).# * Every assigned host port is passed in as GAMEYE_PORT_{PROTOCOL}_{CONTAINER_PORT},# e.g. GAMEYE_PORT_UDP_7777. In "host" network mode your server MUST listen on# those; in "bridge" mode it just listens on its fixed container port.FROM ubuntu:24.04# Run as a non-root user.RUN useradd -m gameuser# Your server build. An invalid ENTRYPOINT stops the container from starting,# so test it locally first.COPY --chown=gameuser:gameuser . /home/gameuser/MyGame/RUN chmod +x /home/gameuser/MyGame/GameServer.shUSER gameuserENTRYPOINT [ "/home/gameuser/MyGame/GameServer.sh", "-log" ]
Source: write · chatcmpl-tool-85d4d0dd8c416e03
Recorded tool call
{
"type": "tool_call",
"seq": 180,
"ts": "2026-10-16T14:56:48.106Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/reference/Dockerfile",
"content": "# Reference only — this repo does NOT build or push images.\n#\n# This mirrors Gameye's minimal game-server template\n# (gameye.com/docs/getting-started/2-docker-file/). Build it yourself and push it\n# to Docker Hub or GHCR, then register it as a Gameye application.\n#\n# docker build --platform linux/amd64 -t <registry>/<you>/my-game:build-001 .\n# docker push <registry>/<you>/my-game:build-001\n#\n# Notes:\n# * Gameye nodes run linux/amd64 — always build for that platform explicitly.\n# * The image must contain everything needed at runtime (server build plus any\n# third-party libs such as anti-cheat or the Steam SDK).\n# * No EXPOSE lines needed: Gameye publishes the ports declared on the\n# application (e.g. UDP 7777).\n# * Every assigned host port is passed in as GAMEYE_PORT_{PROTOCOL}_{CONTAINER_PORT},\n# e.g. GAMEYE_PORT_UDP_7777. In \"host\" network mode your server MUST listen on\n# those; in \"bridge\" mode it just listens on its fixed container port.\n\nFROM ubuntu:24.04\n\n# Run as a non-root user.\nRUN useradd -m gameuser\n\n# Your server build. An invalid ENTRYPOINT stops the container from starting,\n# so test it locally first.\nCOPY --chown=gameuser:gameuser . /home/gameuser/MyGame/\nRUN chmod +x /home/gameuser/MyGame/GameServer.sh\n\nUSER gameuser\n\nENTRYPOINT [ \"/home/gameuser/MyGame/GameServer.sh\", \"-log\" ]\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-85d4d0dd8c416e03",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 181,
"ts": "2026-10-16T14:56:48.106Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 1344 bytes to /sandbox/repo/gameye-example/reference/Dockerfile",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-85d4d0dd8c416e03",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now final verification: run all three commands, check syntax, list files.
Also make sure .env is gitignored — the root .gitignore doesn't have .env. Let me add a .gitignore in the example dir to avoid committing .env (secrets). That's important.
Let me add gameye-example/.gitignore with .env.
Then final checks.
File edits
1 file
Writtengameye-example/.gitignoreView changes
/sandbox/repo/gameye-example/.gitignore
Written content · previous contents were not recorded
# Never commit real credentials..env.env.*!.env.example
Source: write · chatcmpl-tool-bcb153b401908441
Recorded tool call
{
"type": "tool_call",
"seq": 184,
"ts": "2026-10-16T14:56:49.910Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/gameye-example/.gitignore",
"content": "# Never commit real credentials.\n.env\n.env.*\n!.env.example\n"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bcb153b401908441",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 185,
"ts": "2026-10-16T14:56:49.910Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 59 bytes to /sandbox/repo/gameye-example/.gitignore",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-bcb153b401908441",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now final verification run of everything.
cd /sandbox/repo/gameye-example && echo "=== syntax ==="; for f in src/client.mjs src/lifecycle-demo.mjs scripts/verify-spec.mjs; do node --check "$f" && echo " ok $f"; done; echo; echo "=== verify-spec ==="; npm run --silent verify-spec 2>&1 | tail -4; echo; echo "=== files ==="; find . -type f -not -path './node_modules/*' | sort; echo; echo "=== git sees .env as ignored? ==="; printf 'GAMEYE_API_TOKEN=secret\n' > .env && git check-ignore -v .env && rm .env
=== syntax ===
=== syntax === ok src/client.mjs ok src/lifecycle-demo.mjs ok scripts/verify-spec.mjs === verify-spec === Warnings (spec vs docs drift, handled in code): ! LeaveSession.session not in spec schema - docs/api-v2/reference/post-session-player-leave documents `session` as required; the OpenAPI LeaveSession schema lists only `players` with additionalProperties:false. client.mjs sends both. All endpoints and request fields used by this example match the live spec. === files === ./.env.example ./.gitignore ./README.md ./package.json ./reference/Dockerfile ./scripts/verify-spec.mjs ./src/client.mjs ./src/lifecycle-demo.mjs === git sees .env as ignored? === gameye-example/.gitignore:2:.env .env
Recorded tool call
{
"type": "tool_call",
"seq": 188,
"ts": "2026-10-16T14:56:52.107Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/gameye-example && echo \"=== syntax ===\"; for f in src/client.mjs src/lifecycle-demo.mjs scripts/verify-spec.mjs; do node --check \"$f\" && echo \" ok $f\"; done; echo; echo \"=== verify-spec ===\"; npm run --silent verify-spec 2>&1 | tail -4; echo; echo \"=== files ===\"; find . -type f -not -path './node_modules/*' | sort; echo; echo \"=== git sees .env as ignored? ===\"; printf 'GAMEYE_API_TOKEN=secret\\n' > .env && git check-ignore -v .env && rm .env"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8e3ddc5319903770",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 189,
"ts": "2026-10-16T14:56:53.533Z",
"turn": 1,
"output": {
"content": [
{
"text": "=== syntax ===\n ok src/client.mjs\n ok src/lifecycle-demo.mjs\n ok scripts/verify-spec.mjs\n\n=== verify-spec ===\nWarnings (spec vs docs drift, handled in code):\n ! LeaveSession.session not in spec schema - docs/api-v2/reference/post-session-player-leave documents `session` as required; the OpenAPI LeaveSession schema lists only `players` with additionalProperties:false. client.mjs sends both.\n\nAll endpoints and request fields used by this example match the live spec.\n\n=== files ===\n./.env.example\n./.gitignore\n./README.md\n./package.json\n./reference/Dockerfile\n./scripts/verify-spec.mjs\n./src/client.mjs\n./src/lifecycle-demo.mjs\n\n=== git sees .env as ignored? ===\ngameye-example/.gitignore:2:.env\t.env\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-8e3ddc5319903770",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Everything works. Let me do a final summary answer.
Let me verify no node_modules left over and git status is clean-ish.
Let me answer the three questions:
- Built the example ✓
- How pricing works ✓
- Is this product easy for me (the agent) to manage? — Yes, very: pure REST, bearer token, no SDK to embed, public OpenAPI spec, per-second billing with TTL safety net, dry-run mode. The one caveat: some steps require a human/browser (Admin Panel account, token creation, image push).
- Blocked: yes — need GAMEYE_API_TOKEN (and an application with a loaded tag). I could not create an account (app.gameye.com unreachable from here, and signup needs a human).
Let me note that app.gameye.com returned 000 (unreachable). That's an honest detail about where I'm blocked.
Let me do a final git status check quickly.
cd /sandbox/repo && git status --short; echo "--- no stray artifacts ---"; ls gameye-example
?? gameye-example/
?? gameye-example/ --- no stray artifacts --- README.md package.json reference scripts src
Recorded tool call
{
"type": "tool_call",
"seq": 192,
"ts": "2026-10-16T14:56:55.558Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && git status --short; echo \"--- no stray artifacts ---\"; ls gameye-example"
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b574eee461d95564",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 193,
"ts": "2026-10-16T14:56:55.558Z",
"turn": 1,
"output": {
"content": [
{
"text": "?? gameye-example/\n--- no stray artifacts ---\nREADME.md\npackage.json\nreference\nscripts\nsrc\n",
"type": "text"
}
]
},
"run_id": "cmvb38u8j01rc0isn5gxksasl",
"tool_id": "chatcmpl-tool-b574eee461d95564",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Done. Let me write the final summary.
I should be concise but cover the three asks: the example, pricing, manageability, and the blocker.
Done. Built and verified — but I hit one hard blocker at the end (details below).
What I built — gameye-example/
Gameye does have a real developer workflow I could act on: a plain REST Session API. No local stacks, no long-running commands, zero runtime dependencies (Node 18+ global fetch).
gameye-example/
├── src/client.mjs # one method per endpoint, typed JSDoc, error envelope handling
├── src/lifecycle-demo.mjs # runnable end-to-end lifecycle (+ --dry-run)
├── scripts/verify-spec.mjs # validates the example against the LIVE OpenAPI spec
├── reference/Dockerfile # reference only — not built
├── .env.example README.md package.json .gitignore
The demo runs all 8 steps: GET /region → GET /available-location/{image} → POST /session → GET /session/{id} → player join → GET /logs → player leave → DELETE /session/{id} (in a finally, so failures still stop the session).
Nothing here is guessed. I downloaded the live spec (Gameye Session API v1.2.1) and npm run verify-spec confirms all 13 endpoints/methods and every request field against it. Two things that fell out of doing it properly:
- The official
@gameye/sdkon npm is legacy (v0.0.3-0, 2018, "email us for a key");@gameye/match-api-specwon't install (broken dep@oas3/oas3ts-lib). I used the current REST API instead. - Real spec/docs drift:
DELETE /session/player/leave— docs requiresession+players, but the OpenAPILeaveSessionschema declares onlyplayerswithadditionalProperties: false. The client sends both; the verifier reports it as a warning instead of silently passing.
I proved the request path works against the live API using a bogus token — it returned a genuine Gameye error envelope, parsed correctly:
API ERROR 401 UNAUTHORIZED
GET /region -> 401 UNAUTHORIZED: Please verify that you are using a valid auth token.
identifier (give to support): 9b8c474f-...
How pricing works
Per vCPU per hour, billed per second, no minimum session length, zero egress/bandwidth fees. Rate depends on your container's RAM ratio:
| Mode | 1 vCPU : 2 GB | 1 vCPU : 4 GB |
|---|---|---|
| On demand | $0.07 /vCPU/hr | $0.10 /vCPU/hr |
| Reserved (Zen 4/5, min one 8-core machine per region) | $0.035 /vCPU/hr | $0.042 /vCPU/hr |
Cost = container size × seconds alive — not sessions, players, or bandwidth. Billing stops the instant the container terminates, via DELETE or TTL expiry, whichever is first. TTL expiry is an immediate force-kill with no graceful shutdown, so pull artifacts beforehand. Separate BYO-infrastructure model: flat ~$2/vCPU/month on provisioned count. Free trial exists; hitting your credit limit returns 402.
Will this be easy for me to manage?
Yes — unusually easy, for an agent specifically: pure REST with bearer auth, a public OpenAPI spec I can re-verify on demand (so I can catch drift rather than hallucinate), no SDK to embed in a game server, and per-second billing with ttl as a cheap safety net against my own mistakes. The --dry-run mode means I can show you an exact request plan before spending anything.
The caveat is that the setup is human-shaped: Admin Panel account, token creation (shown once), and pushing an image to a registry all need a browser and credentials I don't have.
Blocked — yes, one thing
I need a GAMEYE_API_TOKEN to actually run it. There were no Gameye credentials in the environment, and app.gameye.com is unreachable from here (curl → 000), so I can't self-sign-up either. That's a credentials gap, not a design problem — everything up to the auth boundary is built and verified.
Also note: step 3 needs a Gameye application with at least one tag loaded on nodes, or POST /session 404s. I deliberately did not build/push a Docker image (no daemon, and it needs your registry credentials) — reference/Dockerfile is there as a starting point.
To finish: drop a token and application name into .env, then npm run demo. Run npm run demo:dry right now to see the exact HTTP calls without a token.
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents