Packaging & publishing your game
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Packaging & publishing your game
This guide covers everything between "my game runs locally" and "players can find it": how to package a build, how to create a game listing, the three ways to upload (website, CLI and REST), what moderation checks, what every status means, and how to ship updates, preview builds, roll back and take a game down.
For exact request and response shapes, see Games & uploads API reference. For a five-minute first publish, start with Quickstart.
The lifecycle at a glance
create game (draft)
│
├─ add a cover image ← required before you can submit a build
│
├─ upload init → PUT the .zip → upload complete
│ │
│ bundle checks (instant, synchronous)
│ ├─ fail → version "failed", game stays as it was
│ └─ pass → game "scanning"
│ │
│ automated moderation (typically a minute or two)
│ ├─ auto-approve → "published"
│ ├─ auto-reject → "rejected"
│ └─ needs review → "pending_review" → a human decides
│
└─ later: new version → same checks again (the live version keeps serving) · rollback · unpublishEach upload creates a new version of the game (v1, v2, v3…). The game has one current version, which is the one players get. Moderation looks at each version separately.
1. Package your build
Supported runtimes
Ask the API which runtimes are accepting uploads right now:
curl https://api.coolgptgames.com/v1/runtimesCurrently available:
| What to upload | Published upload limit |
|---|---|---|
| A | 100 MB |
| A | 8 MB |
| A | 10 MB |
| A | 8 MB |
The maxUploadBytes value returned by GET /v1/runtimes is the limit, and upload complete enforces it for each runtime: a bigger zip fails with upload_too_large.
Everything is uploaded as a .zip. PICO-8, Twine and Bitsy builds are also checked for an engine signature (for example, a Twine upload must actually contain Twine story data). If the check fails you get invalid_cart_format with a hint for re-exporting from that tool.
Zip layout and the entry file
Your zip must contain an
index.html(orindex.htm). The file name is case-insensitive.If there is more than one, the one closest to the top of the archive is used. A single top-level folder is fine:
my-game.zip
├── index.html ← entry (preferred: at the root)
├── game.js
├── style.css
└── assets/
├── sprites.png
└── music.oggmy-game.zip
└── dist/
├── index.html ← also fine: the shallowest index.html wins
└── assets/…Use relative paths for everything your game loads (
assets/sprites.pngor./assets/sprites.png). Your game is served from its own folder on our game CDN, so a root-absolute path such as/assets/sprites.pngpoints at the CDN root rather than your build, and it will 404.Your game can't load scripts, images, fonts or media from third-party hosts. Put every asset inside the zip (see Calling external hosts for the one exception).
Allowed file types
Every file in the archive must have one of these extensions (case-insensitive):
Kind | Extensions |
|---|---|
Markup, code & data |
|
Images |
|
Audio |
|
Fonts |
|
3D |
|
Engine data |
|
Pre-compressed files |
|
OS clutter is skipped silently, so you don't have to strip it: __MACOSX/ folders, .DS_Store, Thumbs.db and desktop.ini are simply left out of the stored build.
Any other extension rejects the whole upload with disallowed_extension, and the error names the file. Common culprits:
File | Why it's there | Fix |
|---|---|---|
| Project files | Leave them out of the build folder |
| Video, AAC audio | Remove, or convert (for example to |
Engine exports (Godot, Unity, Emscripten): web exports work as-is, including compressed builds. A file named
x.<type>.brorx.<type>.gzis served as the inner type (soBuild.wasm.bris served as WebAssembly) withContent-Encoding: brorgzip, which is what Unity's loader andWebAssembly.instantiateStreamingexpect. Compressed code is decompressed and scanned by moderation like any other code. A barearchive.gzwith no inner extension is served as an opaque gzip file.
Structural limits
Limit | Value | Error code |
|---|---|---|
Zip size | See the runtime table above |
|
Total size after unzipping | 300 MB |
|
Number of files | 2,000 |
|
Compression ratio of any single file | 100 : 1 |
|
Time to read the archive | 20 seconds |
|
Empty archive | Not allowed |
|
Folders don't count toward the file limit. The compression-ratio limit protects against zip bombs. A normal asset never gets close to it, but a large file of zeros or a huge repetitive text file might.
Path rules
These are rejected for safety:
Problem | Example | Error code |
|---|---|---|
Parent-directory segments |
|
|
Backslashes in names |
|
|
Absolute paths / drive letters |
|
|
Symbolic links | a symlinked |
|
Not a zip at all | a renamed |
|
Archives made on Windows with some older tools store backslashes. Re-zip with a modern tool or from the command line.
Making a clean zip
# macOS / Linux: zip the CONTENTS of your build folder
cd dist
zip -r ../my-game.zip .OS clutter is skipped for you, so no exclusions are needed. Leave out source maps (-x "*.map") if you'd rather not publish your original source.
# Windows: use PowerShell 7+ (older Windows PowerShell 5.1 can write backslashes into zip paths)
Compress-Archive -Path dist\* -DestinationPath my-game.zipBefore you upload, list the archive and check it against the tables above:
unzip -l my-game.zipDesign for the automated play-test
Moderation loads your game in a headless browser, sends some basic input (arrow keys, Space, W/A/S/D and a click near the middle of the screen) and takes screenshots over the first 30 seconds. To pass cleanly:
Draw something immediately. A blank screen after warm-up is an automatic rejection (
blank_screen).A title screen is fine. Make sure a click or key press starts the game, and that something visible is on screen while it loads.
Don't require sign-in, an external account or a network connection before anything is drawn.
2. Create the game listing
A game starts as a draft. Drafts are visible only to you.
Fields
Field | Required | Limit | Notes |
|---|---|---|---|
| Yes | 1 – 120 chars | Also generates the URL slug when the game is created (e.g. |
| No | 4,000 chars | Short description shown on the game page. |
| No | 4,000 chars | "How to play". |
| No | — | One of: |
| No | 160 chars | One-line pitch. |
| No | 8,000 chars | A longer store-page write-up. Helps search ranking. |
| No | Up to 20 entries | Each entry is |
| No | Up to 5 URLs | Your own sites (store page, socials, homepage). Each must start with |
| No | boolean | Shows an "AI-made" label on the game page. Please be honest here. |
| No | 2,000 chars | Optional note on which tools or models you used. |
| No (edit only) | boolean | Default |
websiteUrl(a single URL) is still accepted for older clients and is treated as a one-itemwebsiteslist. Preferwebsites.
Tags. Games can be grouped by tags for "related games", but there is currently no creator-facing way to set tags on your game.
Human verification
On the website, creating a game includes a human-verification check (Cloudflare Turnstile). API clients may omit turnstileToken. If you do send one and it's invalid, the request fails with captcha_failed.
Retry-safe creation
Send an Idempotency-Key header (any unique string, up to 128 characters) when you create a game. If the request is retried with the same key, you get the same game back (200 with "idempotent": true) instead of a duplicate draft.
curl -X POST https://api.coolgptgames.com/v1/games \
-H "Authorization: Bearer $CGG_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: space-dodger-first-publish" \
-d '{
"title": "Space Dodger",
"description": "Dodge asteroids. Grab stars. Don't blink.",
"instructions": "Arrow keys to move, Space to boost.",
"categorySlug": "arcade",
"tagline": "A 60-second arcade dodger",
"websites": ["https://example.itch.io/space-dodger"],
"aiGenerated": true,
"aiDisclosure": "Code drafted with an AI assistant; art hand-drawn."
}'{ "game": { "id": "g_…", "slug": "space-dodger-3f9a1c", "status": "draft" } }Keep the id. Every other creator endpoint uses it.
Editing metadata later
POST /v1/games/{id} takes the same fields (all optional) and updates only the fields you send. Metadata edits take effect immediately and do not trigger re-moderation. They don't create a new version either.
To clear your websites, send
"websites": [].Sending an empty string (
"") for a text field such asdescriptionclears it.
3. Cover, screenshots & loading screen
Images are uploaded as raw bytes to POST /v1/games/{id}/media?kind=…, with the image's content type as the Content-Type header.
| What it is | Count |
|---|---|---|
| The card image in the catalog and on your game page. Required before you can submit a build. | 1 (uploading again replaces it) |
| Gallery images above your description (this is the default if | Up to 8 |
| Shown while your game boots. If unset, the cover is used instead. | 1 (uploading again replaces it) |
Requirements for every image:
Formats: PNG, JPEG, GIF or WebP. The format is detected from the file's contents, not its name or header. Anything else is rejected with
bad_image.Size: at most 5 MB per image (
too_large).Dimensions: none are enforced. Catalog cards display covers at 4:3, so a 4:3 cover (for example 1200 × 900) looks best.
# Cover (required)
curl -X POST "https://api.coolgptgames.com/v1/games/$GAME_ID/media?kind=cover" \
-H "Authorization: Bearer $CGG_API_KEY" \
-H "Content-Type: image/png" \
--data-binary @cover.png
# A screenshot
curl -X POST "https://api.coolgptgames.com/v1/games/$GAME_ID/media?kind=screenshot" \
-H "Authorization: Bearer $CGG_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary @shot1.jpgEach upload returns the image's key and public url. Screenshot uploads also return the full gallery in order. Keep the key: you need it to remove an image or reorder the gallery:
DELETE /v1/games/{id}/mediawith{ "key": "…" }removes a cover, loading screen or screenshot.POST /v1/games/{id}/media/reorderwith{ "keys": ["…", "…"] }sets the gallery order.
If you lost a key, it's the part of the image URL after the CDN host (it starts with media/).
Image uploads are rate-limited to 60 per hour per IP address.
4. Upload a build
Uploading always takes three steps. Every client (website, CLI, SDK) does the same thing behind the scenes:
Init.
POST /v1/upload/initreserves a new version number and returns a one-timeuploadUrlplus anuploadId.PUT. Send the raw zip bytes to
uploadUrl, withContent-Type: application/zip. The URL goes directly to storage, expires after 15 minutes, and takes noAuthorizationheader.Complete.
POST /v1/upload/completewith theuploadId. The API unpacks and validates the bundle right away and returns either an error code from the packaging tables above or{ "status": "scanning" }, which means automated moderation has started.
Via REST (curl)
API=https://api.coolgptgames.com
AUTH="Authorization: Bearer $CGG_API_KEY"
# 1. init
INIT=$(curl -s -X POST $API/v1/upload/init -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"gameId\":\"$GAME_ID\",\"runtime\":\"html5\",\"filename\":\"my-game.zip\"}")
UPLOAD_URL=$(echo "$INIT" | jq -r .uploadUrl)
UPLOAD_ID=$(echo "$INIT" | jq -r .uploadId)
# 2. PUT the bytes (Content-Type must be exactly application/zip)
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/zip" --data-binary @my-game.zip
# 3. complete
curl -X POST $API/v1/upload/complete -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"uploadId\":\"$UPLOAD_ID\"}"
# → {"gameVersionId":"…","status":"scanning"}Via REST (JavaScript)
const API = "https://api.coolgptgames.com";
const headers = {
Authorization: `Bearer ${process.env.CGG_API_KEY}`,
"Content-Type": "application/json",
};
async function call(method, path, body) {
const res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) });
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
return json;
}
// zipBytes: a Buffer / Uint8Array / Blob of your .zip
export async function uploadBuild(gameId, zipBytes) {
const { uploadUrl, uploadId } = await call("POST", "/v1/upload/init", {
gameId,
runtime: "html5",
filename: "my-game.zip",
});
const put = await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": "application/zip" },
body: zipBytes,
});
if (!put.ok) throw new Error(`storage PUT failed: HTTP ${put.status}`);
return call("POST", "/v1/upload/complete", { uploadId }); // { gameVersionId, status: "scanning" }
}Optional fields on init:
Field | Purpose | See |
|---|---|---|
| Turns on multiplayer rooms for this version | |
| External HTTPS hosts this version may call | |
| Declares achievements and XP intent for this version |
Via the website
Upload a game on coolgptgames.com runs the whole flow for you: pick your .zip, fill in the listing, add a cover (required) and up to 8 screenshots, then press Publish game. The page waits a few seconds for the verdict. If moderation is still running, your dashboard updates when it finishes.
Via the CLI
arcadey publish <dir> --cover <image> zips a folder, creates the game, uploads its cover (and any screenshots) and the build, and waits for the verdict. The cover is required for a new game, and the CLI stops before making any API call if it's missing, so you never get a stray draft. It prints the new game's id and the command for shipping updates.
To ship an update to an existing game, add --game <id> (or put "id" in game.json). --cover is optional then and replaces the current cover. --changelog adds release notes.
An optional game.json in the folder can set title, category, description, instructions, realtime, connectHosts, gamification (declared achievements), cover, screenshots and id. See CLI for install, login, every flag and the exit codes.
Via the Publisher SDK
publish({ title, files, cover, … }) creates a game and publishes it with its cover in one call, and publishVersion(gameId, { files, … }) ships an update to an existing game (optionally replacing the cover or adding screenshots). Both wait for the verdict. gamification can be passed on either. Lower-level helpers (uploadCover, uploadScreenshot, uploadVersion) are there if you want each step yourself. See Publisher SDK.
The CLI and the Publisher SDK don't yet send a changelog and a
gamificationblock in the same upload, and refuse the combination. The REST endpointPOST /v1/games/{id}/versionsaccepts both.
Upload rate limits
Starting uploads (upload/init and games/{id}/versions, which share one allowance) is limited per account, per hour, and the allowance grows with your creator level:
Creator level | Uploads per hour |
|---|---|
1 – 2 | 5 |
3 – 5 | 8 |
6 – 9 | 12 |
10 – 14 | 20 |
15 – 24 | 30 |
25+ | 50 |
A failed upload still used up its slot. So validate your zip locally before retrying in a loop. All API calls are also subject to the general per-IP limit (see Errors & limits).
When complete fails
If complete returns an error, that version is marked failed and your game's status doesn't change (a live game stays live). Fix the problem and start again from init. Retrying complete itself after it succeeded is harmless: it returns 200 with "idempotent": true and the version's current state. Each attempt uses a new version number, so it's normal to see gaps like v1, v4.
Error code | Meaning |
|---|---|
| Upload a cover first ( |
| The PUT never happened, failed, or went to an expired URL. |
| The game was delisted or removed after a DMCA notice, and can't take new builds. |
Any code from the packaging tables | The bundle itself is invalid. The error message names the file or limit. |
5. Moderation
Every version goes through the same pipeline. It typically finishes within a minute or two of complete.
What gets checked
Check | What it looks for |
|---|---|
Bundle validation (at | Everything in section 1: format, entry file, file types, sizes, paths. |
Static code scan | Parses every |
Play-test render | Loads the game in a headless browser, plays briefly and captures screenshots. Checks that the game draws something, and flags any network request to a host you didn't declare. |
The three outcomes
Verdict | Happens when | Game not live yet: it becomes | Update to a live game |
|---|---|---|---|
Auto-approve | Every check is clean and confidence is high |
| The new version goes live in place of the old one |
Auto-reject | Any hard-fail signal (table below) |
| Only the new version is |
Needs review | Anything that needs a human's judgement |
| The new version waits ( |
Hard-fail signals (automatic rejection):
Signal | Reason code |
|---|---|
Code or HTML loads from, or sends data to, a host that isn't allowed and isn't declared in |
|
A network request to an undeclared host during the play-test |
|
Known crypto-miner code |
|
Blank screen, or the game isn't playable |
|
Doesn't look like a game at all |
|
Byte-identical to a bundle another creator already uploaded |
|
An exploitable XP or achievement economy |
|
Review signals (sent to a human, not rejected). None of these is forbidden. They just need a human to look:
eval(…),new Function(…),setTimeout("code string"), any.constructor(…)callnew XMLHttpRequest(), orfetch/WebSocketwith a URL built at runtimedynamic
import(), service-worker registrationwindow.top/window.parent,document.cookie,document.writechanging
location,window.open(…),javascript:URLs,<meta http-equiv="refresh">to another hostinline
on…="…"event-handler attributes in HTML.wasmfiles (they can't be statically analysed)heavily obfuscated code (lots of
_0x…identifiers), or JS/HTML that fails to parsepossible sexual content, graphic violence, hate symbols or IP infringement
title or description text that reads as spam
a near-duplicate of another game
low AI confidence in any of the above
Tip: a clean, un-minified or lightly minified build without
eval, inlineonclick=""handlers or runtime-built URLs is much more likely to be auto-approved. Obfuscators almost always send you to human review.
Re-uploading the same zip: your own uploads never count as duplicates, so re-uploading an unchanged zip (for example after a scan hit an error) works.
Reading the result
GET /v1/me/games/{id} (the status page in your dashboard, or arcadey status <gameId>) returns every version with its moderation data:
{
"game": { "id": "g_…", "status": "rejected", "visibility": "full", "currentVersionId": null, "…": "…" },
"versions": [
{
"id": "v_…",
"versionNumber": 2,
"status": "failed",
"isCurrent": false,
"moderation": { "verdict": "needs_review", "confidence": 0.62, "status": "complete", "error": null },
"review": {
"decision": "reject",
"reasonCodes": ["ip_infringement"],
"notes": "Sprites are lifted from a commercial game.",
"at": "2026-09-20T14:02:11.000Z"
}
}
]
}moderationis the automated result:verdict(auto_approve/auto_reject/needs_review),confidence(0 to 1), and jobstatus(queued/running/complete/error).reviewis the decision with itsreasonCodesandnotes. It's filled in for every approval and rejection, whether a human made it or the automated pipeline did. Automated decisions havenotesof"Approved by automated moderation."or"Rejected by automated moderation.", and their reason codes come from the hard-fail signals above. A version held for a human has noreviewuntil the reviewer decides.Every decision also sends a
game.approved/game.rejectedwebhook if you've set one up. The payload includes thegameVersionIdandautomated: trueorfalse(see Webhooks). A human decision also emails you.
Reason codes
Code | Meaning |
|---|---|
| Sexual content or nudity |
| Graphic violence or gore |
| Hateful imagery or harassment |
| Someone else's characters, art, music or brand |
| Doesn't load, or nothing is drawn |
| Not interactive, or not a game |
| A copy or near-copy of an existing game |
| Risky code patterns (eval, obfuscation, cookie or frame access…) |
| Crypto-mining code |
| Talks to hosts it isn't allowed to |
| Fake login or payment screens |
| The title, description or cover doesn't match the game |
| Spammy text, or ads for contact details |
| Real-money gambling mechanics |
| Unsuitable for the general audience |
| Farms XP, achievements or scores |
| See the reviewer's notes |
After a rejection
There is no appeal button. Fix the issue and upload a new version. It goes through moderation again from scratch. If you think the rejection was a false positive (for example a legitimate eval in a scripting engine), removing or isolating the flagged pattern is the most reliable fix.
6. Status reference
Game status
| Meaning | Playable by the public? |
|---|---|---|
| Created, but no build has passed validation yet | No |
| A build passed validation and automated moderation is running | No (see the note below) |
| Automated checks want a human to look | No |
| Live | Yes |
| The game's latest build failed moderation before the game ever went live (a rejected update to a live game doesn't change its status) | No |
| You unpublished it, or a moderator took it down | No |
| Removed after a valid copyright notice | No |
visibility is either full (in the catalog, search and recommendations) or limited (playable by direct link, but hidden from discovery). Approved games are full. A moderator may set a game to limited while looking into player reports.
Updating a live game: your game stays
publishedand keeps serving its current version while the update is moderated. When the update is approved, it goes live in place of the old version (visibility and the original publish date are kept). If it's rejected or held for human review, only the new version is affected: players keep getting the current version. Soscanning,pending_reviewandrejectedonly describe a game that hasn't gone live yet. To follow an update, watch the new version's status inGET /v1/me/games/{id}.
Version status
| Meaning |
|---|---|
| Reserved by |
| Automated moderation is running |
| Cleared the automated checks and is waiting for a human reviewer. (A version approved after its game was taken down also stays |
| Failed bundle validation, automated moderation or human review (or moderation hit an internal error) |
| Approved. It is (or was) served to players. |
7. Updating your game
To ship a new build, upload a new version of the same game. Use either:
POST /v1/upload/initwith the existinggameId, orPOST /v1/games/{id}/versions, which takes the same fields plus achangelog(up to 2,000 chars) shown to players as "What's new".
Both return an uploadUrl / uploadId. Then PUT and complete as usual. Both count against the same hourly upload allowance. The CLI (arcadey publish <dir> --game <id>) and the Publisher SDK (publishVersion(gameId, …)) do all of this for you.
curl -X POST https://api.coolgptgames.com/v1/games/$GAME_ID/versions \
-H "Authorization: Bearer $CGG_API_KEY" -H "Content-Type: application/json" \
-d '{"runtime":"html5","changelog":"Boss fight rebalanced; new level 5."}'
# → 201 { "uploadUrl": "…", "uploadId": "…", "expiresAt": 1790000000000 }Each version gets its own moderation pass, and the game stays live on its current version until the update is approved. Your listing, cover, screenshots, ratings, play counts and leaderboards stay with the game across versions.
Achievements across versions. Both endpoints accept a gamification block (declared achievements). Leave it out and the new version keeps the game's current achievements: the economy audit knows about them, and your existing unlockAchievement calls keep working. Send a block and, on approval, it replaces the game's set: achievements missing from the new block are deactivated (an empty list deactivates them all). A changelog can only be sent on games/{id}/versions.
Changing your game without a new build: remote config values, store items and metadata can all be changed without re-uploading or re-moderation. See Remote config & analytics and In-game economy.
8. Preview builds & instant rollback
Private preview
GET /v1/me/games/{id}/preview returns a play link that only you can generate, for a build that isn't public, such as a draft, a version in review, or a rejected build you want to reproduce.
With no parameters, it previews the current version (or, if nothing is live yet, the newest version).
Add
?versionId=…to preview a specific version.The link has a short-lived access ticket and expires about 15 minutes after it's issued. Request a new one when it expires.
A version that failed at
completehas no build to preview (no_build).
curl "https://api.coolgptgames.com/v1/me/games/$GAME_ID/preview?versionId=$VERSION_ID" \
-H "Authorization: Bearer $CGG_API_KEY"
# → { "playUrl": "https://…?t=…", "version": { "id": "…", "versionNumber": 3, "status": "passed" } }The dashboard's Preview button does the same.
Rollback
POST /v1/me/games/{id}/versions/{versionId}/rollback makes an earlier version current again right away, with no re-moderation. The game becomes published with full visibility.
You can only roll back to a version that was approved and published before (status
live). A version that's still waiting for a human (passed), or one that failed, returns400 not_approved.A game that was delisted or removed after a DMCA notice can't be brought back by rolling back: that returns
409 game_removed. A deleted game returns404.Use it to undo a bad update that was approved.
await fetch(
`https://api.coolgptgames.com/v1/me/games/${gameId}/versions/${versionId}/rollback`,
{ method: "POST", headers: { Authorization: `Bearer ${process.env.CGG_API_KEY}` } },
).then((r) => r.json());
// → { ok: true, currentVersionId: "…", versionNumber: 2 }9. Unpublishing
DELETE /v1/games/{id} (the Unpublish button in the dashboard) removes the game from the catalog and from your games list:
Its status becomes
delistedand the game page stops loading for everyone, including you.It works on drafts as well, to get rid of an abandoned draft.
There's no self-serve undo. Once unpublished, the game no longer appears in
GET /v1/me/gamesand its manage view returns 404. If you want a game back online later, don't delete it. Just stop promoting it, or roll back to a version you're happy with.
Once a game is unpublished, every write to its id (metadata edits, new uploads and versions, preview, rollback and images) returns 404.
A moderator can also delist a game (for example after player reports). That shows up as status delisted in your dashboard, while the game still appears in your list. A delisted game can't take new builds or be rolled back (409 game_removed).
10. Copyright (DMCA) from a creator's point of view
Anyone can file a copyright takedown notice against a game through the DMCA form on coolgptgames.com. Filing a notice doesn't remove anything by itself. A person reviews it first.
If the notice is valid, the game's status becomes
dmca_removedand it stops being playable. New uploads and rollbacks for it are refused (409 game_removed), so a removed game can't be republished from your side.If you believe the removal is a mistake (you own the work, have a license, or it's fair use), you can send a counter-notice. Under the DMCA, removed content may be restored if the claimant doesn't take legal action within the statutory window. See the DMCA & Copyright policy on coolgptgames.com. - Repeat infringers are terminated: three substantiated strikes close the account.
Don't re-upload removed content under a new game. Duplicate detection and human review both look for it, and it counts toward the strike policy.
To avoid problems in the first place: use only assets you made, licensed or that are clearly public domain / CC0. Don't use other games' characters, logos or music. Credit licensed assets in your description.
Calling external hosts (connectHosts)
Games run in a locked-down sandbox and can only reach our own services by default. If your game needs a public HTTPS API (say, a word list or an emoji service), declare the host on upload/init or games/{id}/versions:
{ "gameId": "g_…", "runtime": "html5", "filename": "my-game.zip",
"connectHosts": ["https://api.example.com"] }Rules (a violation fails init with bad_connect_hosts):
Exact HTTPS hosts only. A bare host such as
api.example.comis treated ashttps://api.example.com. Paths are ignored, since the declaration is per host.No wildcards, IP addresses,
localhost, single-word hosts, or internal names (.local,.internal,.lan,.corp…).No coolgptgames.com origins. They're already allowed.
At most 10 hosts per version (
too_many_connect_hosts).The API you call must send
Access-Control-Allow-Origin: *and must not need cookies or credentials. Your game's origin is opaque (null).Declared hosts are checked by moderation on every version. Declare them again each time you upload.
Moderation treats declared hosts as allowed: a literal
fetch("https://api.example.com/…")to a declared host passes the code scan and the play-test. Requests to any undeclared host are still flagged.
How the browser enforces this is covered in How games run. If the data is static, just put it in your zip as a .json file: no network, no CORS, nothing to review.
Troubleshooting checklist
Symptom | Likely cause |
|---|---|
| Upload |
| No |
| A file type that isn't on the allowlist, such as |
| Not a real zip, or a truncated upload. Re-zip and upload again. |
| The PUT was skipped, failed, or the 15-minute URL expired. Start again from init. |
Storage PUT returns 403 |
|
| Hourly upload limit reached. Wait, and validate locally before retrying. |
| The game was delisted or DMCA-removed. It can't take new builds or be rolled back. |
Instantly | Your zip is byte-identical to another creator's upload. |
Stuck in | A human reviewer will decide. Check the review signals above to avoid it next time. |
Update doesn't show up for players | It's still in moderation (the old version keeps serving until it's approved), or it was rejected. Check the new version's status in |
Assets 404 in the preview | You're using root-absolute paths ( |