# RunXR — building games for the platform (spec for AI agents) > RunXR (https://runxr.app) is a controller-first web game portal. Any HTML5 game that runs in a browser can be listed. A game is one catalog entry (title, artwork, metadata) plus a URL that RunXR loads in a full-screen iframe. Human guide: https://runxr.app/docs ## Two ways to provide a game 1. Hosted (preferred, "Poki-style"): the game lives on your own HTTPS host (Vercel, Netlify, GitHub Pages, itch.io...). Catalog URL = full https:// address. 2. Uploaded: static files placed under RunXR's `games/` folder, served at `/games/.html` with the right headers. Catalog URL = `/games/your-game.html`. ## Hard requirements (a game is rejected if these fail) - Serve over HTTPS. No mixed content. - Must load inside an iframe: do NOT send `X-Frame-Options: DENY|SAMEORIGIN`; if you send a CSP it must include `frame-ancestors https://runxr.app`. - Never navigate the top window (`window.top.location`, `target="_top"`); never assume a fixed viewport — the frame is full-screen at any aspect ratio, read `innerWidth/innerHeight` and listen to `resize`. - Fully playable with a gamepad only (every screen: title, menus, pause, game over). Keyboard fallback must also work. No mouse-only or text-input-only flows. - Reserved input, never bind these: `Esc`, `Start + Select` together, the Guide/Home button. All three open the RunXR Quick Menu (Resume / Restart / Fullscreen / Favorite / Quit) over the game. - Pause when told: freeze gameplay, timers and audio on `gs-pause` (`RunXR.onPause`), continue on `gs-resume` (`RunXR.onResume`). The game keeps running behind the Quick Menu otherwise. - Start = pause. A = confirm/primary. B = back/cancel. D-pad or left stick = move/navigate. ## Iframe permissions RunXR grants the game `gamepad, fullscreen, autoplay, accelerometer, gyroscope, xr-spatial-tracking` (pointer lock is not granted). Audio still needs a user gesture: create/resume the AudioContext on the first button press. ## The RunXR SDK (optional, recommended) Include once: `` — inert outside RunXR, safe to ship permanently. - `RunXR.ready()` — call after the first frame renders; hides the launcher's "how to exit" toast. - `RunXR.score(n)` — call whenever the score changes; RunXR keeps each signed-in player's best per game and shows it on the detail card. - `RunXR.menu()` — open the RunXR Quick Menu (from your own pause screen). - `RunXR.exit()` — quit straight back to the RunXR menu (wire to your own Quit button). - `RunXR.onPause(fn)` / `RunXR.onResume(fn)` — Quick Menu opened / closed; freeze and unfreeze the game. - `RunXR.paused` — boolean, true while the Quick Menu is open. - `RunXR.embedded` — boolean, true only inside RunXR. The SDK also binds Esc, Start+Select and Guide to menu() and posts `gs-hello` on load. `window.GameStation` is a legacy alias. ## postMessage protocol (what the SDK does; any game may post these to window.parent with target origin "*") Game -> RunXR: - `{ type: 'gs-hello' }` informational - `{ type: 'gs-ready' }` hides the exit hint toast - `{ type: 'gs-score', score: }` records the score (best is kept) - `{ type: 'gs-menu' }` or `{ type: 'gp-menu' }` open the RunXR Quick Menu - `{ type: 'gs-exit' }` or `{ type: 'gp-exit' }` quit straight to the RunXR menu RunXR -> game: - `{ type: 'gs-pause' }` Quick Menu opened: freeze - `{ type: 'gs-resume' }` Quick Menu closed: continue RunXR -> game (same-origin uploaded games only): - `{ type: 'gp-key', key: , down: }` forwarded keyboard events ## Input mapping (Gamepad API standard layout, button indices) A=0 B=1 X=2 Y=3 LB=4 RB=5 LT=6 RT=7 SELECT=8 START=9 UP=12 DOWN=13 LEFT=14 RIGHT=15 HOME=16; left stick = axes[0] (x), axes[1] (y), use a ~0.25 dead zone. Keyboard mirror: Arrows/WASD -> D-pad · Space/Enter/Z -> A · X -> B · C -> X · V -> Y · Q/E -> LB/RB · P -> Start · Shift -> Select · Esc -> Quick Menu. RunXR's own prompts adapt to Xbox (A/B/X/Y), PlayStation (✕○□△) and Nintendo (B/A/Y/X) pads and can put confirm on the right face button; games should still read the standard mapping (index 0 = bottom face button). Poll `navigator.getGamepads()` once per frame; track "held" (down now) vs "pressed" (down now, up last frame). ## Uploaded games: helper library `` gives `Pad` — `poll() held(btn) pressed(btn) axis(0|1) dir() vdir() menu() exit() sysPaused()` (GameKit.loop freezes while sysPaused()) with buttons `A B X Y LB RB LT RT START SELECT UP DOWN LEFT RIGHT HOME` — plus a touch overlay (thumb-stick, A, B, X, Pause, Menu) on phones/tablets, and `GameKit` — `setup(w,h) loop({update,draw,w,h,ctx}) text() overlay() best(key) setBest(key,v) beep() rand(a,b)`. ## Rendering & UX rules - Render to a fixed logical resolution (e.g. 960x540 or 1280x720) on a canvas and letterbox it with CSS to the window. Set `touch-action: none` on the canvas; hide overflow. - Title screen starts on A (not on click). Show a short controls hint. Large, high-contrast text readable from a couch; no hover-only UI. - Save progress/high scores in localStorage. Keep load small; show a loading state. - Touch: to tick Mobile Phone / Tablet, ship on-screen controls (uploaded games get them from pad.js automatically). - Engines: Phaser, PixiJS, Three.js, Godot, Unity WebGL, Construct, GDevelop, PlayCanvas, Babylon.js all work if the exported page follows the rules above. ## Making an EXISTING game compatible — checklist 1. Embeddable: HTTPS + no frame-blocking headers (test with a local `