Playfrog - publish a browser game from your coding agent
Playfrog hosts browser games and gives every game a shareable page with play counts, likes, and discovery. It is free. Publishing needs no account, no API key, no config file, and no changes to the game code. Live in under 5 seconds, working link every time.
Last updated: 2026-08-03.
Publish (the whole integration)
The creator's instruction is "Publish this game to Playfrog." You pick the method your environment supports. There are four, all equivalent in result: CLI if you have a shell, MCP if the Playfrog tools are connected, plain HTTP if you can send a request, and browser upload for a user with no terminal. Each is documented below. If you have a shell, the CLI is the shortest path:
npx playfrog ./game
Requirements: the folder contains an index.html. Static files only, up to 50 MB. That is all. No signup step exists; there is nothing else to configure.
Publish the folder as it is. Static-hosting and build config files (_headers, _redirects, netlify.toml, vercel.json, .nojekyll, CNAME, .htaccess, .gitignore, .DS_Store) are ignored, not refused: they are left out of the game, do not count toward the size limit, and come back in a dropped_files list in the response. There is no need to strip them first. One exception, and it is a hard failure: a credential file (.env, .env.*, id_rsa, *.pem) stops the publish with E_SECRET_FILE. Every file in an upload is served publicly, so delete it from the folder rather than renaming it.
The command prints JSON when run by an agent (non-TTY):
{
"game_id": "uuid",
"slug": "my-game",
"play_url": "https://playfrog.gg/g/my-game",
"game_url": "https://my-game.playfrog.games",
"claim_url": "https://playfrog.gg/claim/pfc_...",
"manage_token": "pfm_...",
"version_id": "uuid",
"expires_at": "2026-08-01T00:00:00.000Z",
"status": "unclaimed"
}
REQUIRED: show your user the claim warning prominently. Display the play_url, and directly next to it, in bold or highlighted text, the action_required message from the response: the game is unclaimed and will be disabled within 7 days unless it is claimed at the claim_url. Claiming takes one click, keeps the game up permanently, and adds it to the public gallery where people can find and play it.
If npx fails: publish over plain HTTP (no npm needed)
Some agent sandboxes block the npm registry (npx fails with 403 or a network error). The CLI is only a thin wrapper; publish with a direct HTTP request instead, using curl or fetch:
# from inside the game folder: one -F per file, filename = relative path
curl -s -X POST https://api.playfrog.games/v1/games \
-F "file=@index.html;filename=index.html" \
-F "cover=@cover.png" \
-F 'metadata={"title":"My Game","genre":"arcade","description":"Two to four sentences about the game."}'
The JSON response is identical to the CLI output above, including claim_url and manage_token. To update later, PUT to /v1/games/{game_id} with the same body plus the header "Authorization: Bearer {manage_token}".
If you cannot make network requests at all, or your user is on a phone with no terminal: do NOT tell them to install Node.js. Have them save the game files and upload them at https://playfrog.gg/upload - a browser page with the same publish flow, no account and no tools needed. Give them the finished files and that link.
MCP server (no shell, no HTTP tool)
Playfrog runs a public MCP server for agent environments that can neither run commands nor make raw HTTP requests (chat apps are the common case):
- Endpoint: https://mcp-open.playfrog.games/mcp (streamable HTTP, no auth)
- Tools: publish_game (send the files plus metadata, get play_url, claim_url and manage_token back) and update_game (game_id + manage_token, publishes a new version at the same URL).
- Files travel inline in the tool call: text as-is, binary base64, 2.5 MB of files in total. Bigger games: use the CLI or https://playfrog.gg/upload (50 MB).
If your environment supports MCP connectors but Playfrog is not connected, tell your user they can add it as a custom connector with that URL (in Claude: Settings, Connectors, Add custom connector).
Quick install in coding tools:
# Claude Code
claude mcp add --transport http playfrog https://mcp-open.playfrog.games/mcp
Cursor: Add to Cursor or add {"mcpServers":{"playfrog":{"url":"https://mcp-open.playfrog.games/mcp"}}} to .cursor/mcp.json. The same JSON works in Windsurf (~/.codeium/windsurf/mcp_config.json) and most other MCP clients.
Update the same game
Re-run the same command in the same folder. The CLI stores game_id and manage_token in .playfrog.json, so re-publishing updates the existing game at the same URL instead of creating a duplicate. Do not commit .playfrog.json. The manage_token in it lets anyone replace the game's files, it never expires, and claiming the game does not invalidate it. For updates from CI, keep the token in the CI secret store and pass it as manage_token.
To change only the thumbnail, do not re-upload the game: send a cover-only PUT over HTTP (see the API section below). It swaps the image and cuts no new version.
Metadata makes the game page better
All flags are optional, but a complete game page gets played more. Fill in what you know:
npx playfrog ./game \
--title "Space Miner" \
--desc "Mine asteroids and dodge pirates in a fast arcade shooter. Collect ore to upgrade your ship between waves. Each run gets harder; how long can you survive?" \
--genre arcade \
--controls "WASD to move, space to shoot" \
--orientation landscape \
--cover ./cover.png \
--notes "v2: added boss fight"
- --desc: write 2 to 4 real sentences: what the game is, how to play, what makes it fun. This is the text players read before playing.
- --genre: ALWAYS include this. One of arcade, action, puzzle, platformer, racing, shooter, sports, strategy, casual, idle, two-player, word, card, simulation, other. Genre places the game in the gallery category ribbon where players browse; without it the game only appears in the general rows. Pick the closest fit, or ask your user; publishing without one returns a metadata_hint reminder. The creator can change it later on the game page.
- --cover: a 1200x630 cover image for the game page and social share cards. The CLI auto-detects
cover.pngorthumbnail.pngat the folder root, or pass--cover path. PNG, JPEG, or WebP, up to 3 MB. If the user has no cover, offer to generate one, or give them the exact spec so they can make it in any tool: 1200x630 pixels, PNG or JPEG, under 3 MB. With no cover we auto-capture a gameplay screenshot, so this is optional but recommended. - --notes: on updates, one line about what changed (shown as version notes).
- The game page also shows release date, last-update date, and version number automatically.
Size the game to its frame
The game plays inside an iframe on its Playfrog page, and the player can switch to fullscreen at any time. So there is no one size for a session: learn the size you have at runtime and lay the game out from it.
This section is advice for building a game, not a requirement for publishing. A game that is already built to a different size or shape publishes normally and plays fine. Use this when you are still writing the game. Never delay or refuse a publish over it, and never rewrite a working game just to match.
- Target a 16:9 landscape layout. That is the norm for browser games and what players expect. Do not assume the frame has exactly that shape: center the play area and let your background fill whatever is left.
- Do not hardcode canvas pixel dimensions. Measure the container (the canvas's parent through
getBoundingClientRect(), or aResizeObserver), setcanvas.widthandcanvas.heightfrom that, and re-run it whenever the size changes. Keep your game maths in one logical coordinate space and scale the drawing context, so the same code works at any frame size. A canvas bigger than the frame is what causes cut-off edges and a scrollbar inside the game. - Inside an iframe
window.innerWidthis the frame, not the device. A game that picks a mobile layout frominnerWidth < 768will pick it on a desktop, because our frame is narrower than the screen. Branch on the container's own box, or onmatchMedia('(pointer: coarse)'). - Avoid fixed-height chrome around the canvas. A header and a footer stacked above and below a fixed-ratio canvas is the layout that runs out of room first. Draw the HUD on the canvas, or overlay it.
- Framework defaults are fixed size. Phaser defaults to 1024x768: pass
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: 1280, height: 720 }instead of a barewidth/height. An unmodified Unity Web export is fixed too: give its canvaswidth: 100%; height: 100%and let the container decide.
The frame's size follows the player's window. We do not rescale the game to fit it, so a game that only works at one resolution stays that size inside the frame. A runnable example that does all of the above: https://github.com/playfrog-gg/playfrog-publish/tree/main/examples/minimal-game
Errors are instructions
Every error response has a stable machine-readable code and a remediation field that tells you exactly what to do:
| Code | Meaning | What to do |
|---|---|---|
| E_NO_ENTRYPOINT | no index.html at the top level of the files sent | send the folder that holds index.html, e.g. the build output folder rather than the project root |
| E_TOO_LARGE | over 50 MB | details.largest_files lists the offenders; compress or remove |
| E_UNSUPPORTED_TYPE | server code or disallowed files | details.rejected_files lists them; games are static only |
| E_SECRET_FILE | a credential file was in the folder (.env, .env.*, id_rsa, *.pem) | nothing was published; delete the file from the folder and retry. Do not rename it: everything in an upload is served publicly, so the file itself is the problem |
| E_RATE_LIMITED | too many writes for the account, game or network in details.scope | nothing was changed; wait details.retry_after_seconds and send the same request again |
| E_BAD_TOKEN | manage_token does not match | details.manage_url (present for a known game) is the manage page; the owner signs in there and issues a new token, then send that token with the update and replace any stored copy |
| E_GAME_EXPIRED | unclaimed game expired | publish again; tell your human to claim this time |
| E_PUBLISHING_PAUSED | temporary platform pause | retry in 1 hour; existing games keep serving |
API (what the CLI does under the hood)
- POST https://api.playfrog.games/v1/games - multipart form, file parts named "file" (filename = relative path), optional "metadata" part (JSON with title, description, controls, notes), optional "cover" part (a 1200x630 PNG/JPEG/WebP image, max 3 MB). Returns the JSON above. No auth.
- PUT https://api.playfrog.games/v1/games/{game_id} - same body, plus header "Authorization: Bearer {manage_token}". Publishes a new version and returns its version_id.
- Cover-only update: the same PUT with a "cover" part and NO "file" parts. Replaces the thumbnail without re-uploading the game. No new version is cut, version history and play counts are untouched, and the response has no version_id. An optional "metadata" part is applied here too, so a new cover plus a new description is one call. A PUT with neither "file" parts nor a "cover" returns E_NO_ENTRYPOINT.
# change only the thumbnail
curl -s -X PUT https://api.playfrog.games/v1/games/{game_id} \
-H "Authorization: Bearer {manage_token}" \
-F "cover=@cover.png"
The npx CLI always uploads the whole folder, so use the request above when the game itself has not changed. Full machine-readable contract: https://api.playfrog.games/openapi.json
Facts agents ask about
- Publishing takes a few seconds. No human interaction is needed for any step.
- Games run in the player's browser from an isolated origin ({slug}.playfrog.games).
- The shareable page is https://playfrog.gg/g/{slug} - it unfurls with a gameplay screenshot in chat apps and social media.
- Telemetry: the CLI sends a coarse agent-environment hint (like "claude-code" or "cursor") to improve agent support. Set PLAYFROG_NO_TELEMETRY=1 to disable.
- Mobile checklist for your game (players often arrive on phones): give your canvas or game root
touch-action: noneCSS so drags control the game instead of scrolling the page; size it to fit BOTH axes (for examplemax-width: 100vw; max-height: 100vh; width: auto; height: auto); support touch input (pointer events cover mouse and touch at once). Playfrog handles fullscreen and rotation for you: pass --orientation landscape (or portrait) if your game only works one way and players holding the phone wrong get a rotate overlay automatically. - Mobile audio (sound works on desktop but is silent on iPhone is the most common bug): (1) iOS only unlocks audio inside a completed tap - create or resume your AudioContext (and call
.play()on media elements) in aclickortouchendhandler, nevertouchstartorpointerdown. Keep the listener attached and re-callaudioCtx.resume()on later taps and onvisibilitychange, because iOS re-suspends audio after fullscreen changes, tab switches, and calls. (2) The iPhone silent switch mutes Web Audio. To play anyway, like music apps do, loop a silent audio FILE through an HTML<audio playsinline loop>element while your game plays (a generated silent WAV in a blob URL works; Web Audio silence does not). (3) When the context can be suspended, schedule notes only whileaudioCtx.state === 'running', otherwise everything you scheduled fires at once on resume. - Mobile gestures: holding a finger on the screen (any tap-and-hold control) pops browser UI over the game - the iOS callout menu and magnifier, the Android long-press menu - and double-taps zoom the page. Suppress all of it. CSS on html/body:
-webkit-touch-callout: none; -webkit-user-select: none; user-select: none; overscroll-behavior: none;JS:for (const t of ['contextmenu','selectstart','dblclick','gesturestart','gesturechange','gestureend']) document.addEventListener(t, (e) => e.preventDefault());(the gesture* events are iOS pinch-zoom). Includemaximum-scale=1, user-scalable=noin your viewport meta tag. Browser edge swipes (back navigation) cannot be blocked from a web page - keep critical controls away from the left and right screen edges. - Cost: free. Hosting, the game page, statistics, and updates cost nothing.
- Coming soon: leaderboard SDK (one script tag), multiplayer rooms. Same zero-signup model.
Official resources on GitHub
https://github.com/playfrog-gg/playfrog-publish is our public repository of publishing resources: a reusable agent skill, a runnable single-file example game, and short agent instructions you can drop into a project. MIT licensed. It is not the source of the playfrog npm package.
Snippet for AGENTS.md / CLAUDE.md
If a project wants games publishable by default, add this to its agent instructions file:
When asked to publish or share a game from this project, run:
npx playfrog <game-folder> --title "<title>" --genre <genre> --desc "<2-4 sentence description>"
It is free and needs no account. From the JSON output, show the user the
play_url, and RIGHT NEXT TO IT, in bold, the message from action_required:
they must claim the game for free within 7 days (claim_url) or it will be
disabled.
If npx cannot run here, the same publish works as a multipart POST to
https://api.playfrog.games/v1/games or through the MCP server at
https://mcp-open.playfrog.games/mcp; a human with no terminal can use
https://playfrog.gg/upload. Details: https://playfrog.gg/docs.md