← MindblownCONTENT HISTORY

Update to Mindblown

Snapshot Nov 3, 2026 · 00:13 UTC · version 1.0.1

Collection source: downloaded plugin package.

WHAT CHANGED

Instructions updated for mindblown

Skill instructions — Now says: `create_test_seats`, `get_page_errors`, `get_page_stats`). | `GET /me/games/GAME_ID/errors` with `sha` | `get_page_errors` with `version` | | `GET...

Observed in instructions or declared skills. Runtime behavior has not been tested.

Save Mindblown and check future changes in Following.

Up to 10 plugins free · no card required.

All fields and original values.

Skill instructions

Before

Oct 31, 2026 · 01:15 UTC

`create_test_seats`). password, and there is no API key to store or leak. this names, and choosing it is theirs — the same rule as listing a page. counts — `play_count`, `unique_players`, `last_played_at`. There is no separate stats call...

After

Nov 3, 2026 · 00:13 UTC

`create_test_seats`, `get_page_errors`, `get_page_stats`). | `GET /me/games/GAME_ID/errors` with `sha` | `get_page_errors` with `version` | | `GET /me/insights` with `game_id` | `get_page_stats` with `page_id` | password. An API ke...

Supporting files

Before

Oct 31, 2026 · 01:15 UTC

[{"relative_path":"references/generate.md","size_in_bytes":3743},{"relative_path":"references/inventory.md","size_in_bytes":12246},{"relative_path":"references/sdk.md","size_in_bytes":37475}]

After

Nov 3, 2026 · 00:13 UTC

[{"relative_path":"references/generate.md","size_in_bytes":3743},{"relative_path":"references/insights.md","size_in_bytes":8480},{"relative_path":"references/inventory.md","size_in_bytes":13395},{"relative_path":"references/sdk.md","size...

Compare saved observations

Download comparison JSON

SKILL.md text changes

− removed · + added

--- before
+++ after
@@ -13,7 +13,7 @@
 
 1. **The mindblown tools are connected** (`publish`, `publish_status`,
    `get_page_files`, `list_projects`, `list_versions`, `release_version`,
-   `set_listed`, `create_test_seats`).
+   `set_listed`, `create_test_seats`, `get_page_errors`, `get_page_stats`).
    Use them. The host signs the person in when a tool asks for it, so skip
    [Sign in](#sign-in) and never ask for or keep a token. Each tool is one
    call below:
@@ -27,6 +27,8 @@
    | `POST /me/games/GAME_ID/release` with `sha` | `release_version` with `version` |
    | `PATCH /me/games/GAME_ID` with `listed` | `set_listed` |
    | `POST /me/test-seats/GAME_ID` | `create_test_seats` |
+   | `GET /me/games/GAME_ID/errors` with `sha` | `get_page_errors` with `version` |
+   | `GET /me/insights` with `game_id` | `get_page_stats` with `page_id` |
 
    A publish is three calls: `publish`, then `publish_status` until it is
    done, then `open_page` with the `page_id` to show the person the live page.
@@ -73,7 +75,8 @@
 ## Sign in
 
 mindblown uses the OAuth 2.0 device flow (RFC 8628). You never handle a
-password, and there is no API key to store or leak.
+password. An API key exists for assets only (see "Make assets"); it cannot
+publish.
 
 ### 1. Ask for a code
 
@@ -440,7 +443,7 @@
 of these. A build this page never published is refused with a 404.
 
 **Ask the person first, every time.** Whoever opens the page gets whichever build
-this names, and choosing it is theirs — the same rule as listing a page.
+this names, and choosing it is theirs — the same rule as making a page public.
 
 ## Your pages
 
@@ -450,8 +453,7 @@
 ```
 
 Each row carries the page's `slug`, `title`, `play_url`, `state`, and its play
-counts — `play_count`, `unique_players`, `last_played_at`. There is no separate
-stats call; the numbers are here.
+counts — `play_count`, `unique_players`, `last_played_at`.
 
 The history of one page:
 
@@ -460,6 +462,52 @@
   -H "Authorization: Bearer $TOKEN"
 ```
 
+What visitors hit on a page, how the pages are doing day by day, and recordings
+of how people used them: `references/insights.md`. Read it when
+the person asks what to fix or how a page is doing.
+
+## Make assets
+
+Pictures, cutouts, 3D models (GLB), rigged characters, sound effects, speech
+and video clips, made for the page you build. They are paid from the person's
+mindblown tokens, at cost. The account must have topped up or subscribed at
+least once; if not, the answer is `402` with `top_up_url`. Send the link to
+the person.
+
+Use the device-flow token, or an API key the person makes in Settings, on
+<https://mindblown.ai>. A key starts `mb_` and works on these two routes only.
+
+```bash
+curl -X POST https://api.mindblown.ai/me/assets \
+  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
+  -d '{"kind":"image","prompt":"a small red robot, full body, plain background"}'
+# 202 {"id":"…","status":"queued",…}
+
+curl https://api.mindblown.ai/me/assets/ID -H "Authorization: Bearer $TOKEN"
+# {"status":"done","url":"https://play.mindblown.ai/__mb/gen/<sha>.png","cost_usd":0.0528,…}
+```
+
+Poll every few seconds. A picture or a sound takes under a minute; a 3D model,
+a rig or a clip can take several. `status` is `queued`, `running`, `done` or
+`failed` (with `error`). The `url` is public and permanent: put the file in the
+page, or load it from the page.
+
+| `kind` | Needs | Optional | File |
+|---|---|---|---|
+| `image` | `prompt` | `size` (`square`, `wide`, `tall`), `image_url` to edit a picture | png |
+| `cutout` | `image_url` | | png |
+| `model3d` | `image_url` | `faces` (40,000–1,500,000), `pbr` | glb |
+| `rig` | `model_url` (at most 300,000 faces) | `height_meters` | glb |
+| `sound` | `prompt` | `seconds` (0.5–22), `loop` | mp3 |
+| `speech` | `prompt` (the words) | `voice` | mp3 |
+| `video` | `prompt` | `image_url` to animate, `seconds` (5–15), `resolution` (`480P`, `768P`) | mp4 |
+
+A 3D model is made from a picture: make an `image`, then pass its `url` as
+`image_url`. The subject must fill at least half of that picture, or the model
+fails: ask for one object, full body, on a plain background. An `image_url` or
+`model_url` must be `https`. At most 8 assets of one account are made at a
+time; the 9th answers `429`.
+
 ## What runs inside a published page
 
 Every page we serve carries one object of ours, `window.mindblown`, injected at
@@ -484,10 +532,12 @@
 | `references/inventory.md` | takes the person's own car: a slot and our picker |
 | `references/generate.md` | makes text or pictures from what a viewer types |
 
-## The creator's public page
+## Private and public
 
-A published page is live at its own URL immediately. Whether it also appears on
-the person's public page is a second, separate choice, and it is theirs:
+A published page is live at its own URL immediately, and it starts **private**:
+anyone with the link can open it, and it is not on the person's profile, in
+the feed or in search. Making it **public** puts it there. That is a second,
+separate choice, and it is theirs:
 
 ```bash
 curl -X PATCH https://api.mindblown.ai/me/games/GAME_ID \
@@ -496,14 +546,15 @@
   -d '{"listed": true}'
 ```
 
-Do not list a page without asking the person first.
+Do not make a page public without asking the person first.
 
 ## What you should tell the person
 
 Two things, before you publish for them:
 
-1. **A published page is public.** Anyone with the link can open it. There is no
-   private mode. Do not publish anything they would not put on the open web.
+1. **A published page opens for anyone with the link.** Private only keeps it
+   off their profile, the feed and search; it does not lock the link. Do not
+   publish anything they would not put on the open web.
 2. **It is on their account.** They signed in; the page is theirs, not yours.
    They can see it and manage it without you.
 
Full technical diff · 2 changed fields

changed /included_files

BEFORE
[
  {
    "relative_path": "references/generate.md",
    "size_in_bytes": 3743
  },
  {
    "relative_path": "references/inventory.md",
    "size_in_bytes": 12246
  },
  {
    "relative_path": "references/sdk.md",
    "size_in_bytes": 37475
  }
]
AFTER
[
  {
    "relative_path": "references/generate.md",
    "size_in_bytes": 3743
  },
  {
    "relative_path": "references/insights.md",
    "size_in_bytes": 8480
  },
  {
    "relative_path": "references/inventory.md",
    "size_in_bytes": 13395
  },
  {
    "relative_path": "references/sdk.md",
    "size_in_bytes": 39009
  }
]

changed /skill_md_contents

BEFORE
"---\nname: mindblown\ndescription: \"Publish browser games and interactive pages to the web with Mindblown. Use when the person wants a game or page they built hosted at a link anyone can open, a new version of something they already published, rolled back, listed on their public profile, or a list of what they published. Mindblown checks it in a real browser before it goes live.\"\n---\n<!-- GENERATED by scripts/build-plugin-skills.py from mindblown-api/internal/httpapi/skillapi/skill.md. DO NOT EDIT. -->\n\n# mindblown\n\n## How to reach mindblown from here\n\nUse the first way that is available. Everything else in this file applies to\nboth.\n\n1. **The mindblown tools are connected** (`publish`, `publish_status`,\n   `get_page_files`, `list_projects`, `list_versions`, `release_version`,\n   `set_listed`, `create_test_seats`).\n   Use them. The host signs the person in when a tool asks for it, so skip\n   [Sign in](#sign-in) and never ask for or keep a token. Each tool is one\n   call below:\n\n   | HTTP call in this file | Tool |\n   |---|---|\n   | `POST /publish` | `publish` with `files: [{path, content, encoding}]`, `title`, `page_id` |\n   | `GET /publish/<job>` | `publish_status` with `job_id` |\n   | `GET /me/games` | `list_projects` |\n   | `GET /me/games/GAME_ID/versions` | `list_versions` |\n   | `POST /me/games/GAME_ID/release` with `sha` | `release_version` with `version` |\n   | `PATCH /me/games/GAME_ID` with `listed` | `set_listed` |\n   | `POST /me/test-seats/GAME_ID` | `create_test_seats` |\n\n   A publish is three calls: `publish`, then `publish_status` until it is\n   done, then `open_page` with the `page_id` to show the person the live page.\n\n   Every tool takes the publish `page_id` where the HTTP call takes\n   `GAME_ID`; the tool finds the page itself. Send text files with\n   `encoding: \"utf8\"` and every other file with `encoding: \"base64\"`.\n\n   **To change a page that is already published**, you do not have its files\n   from an earlier turn. Call `get_page_files` with its `page_id`, edit the\n   files, then `publish` ALL of them with the same `page_id`, poll\n   `publish_status` until it is done, and call `open_page`. The file objects\n   go back into `publish` unchanged. Leave the tags mindblown put in the HTML (a script tag, `og:`\n   meta, the favicon); publishing puts them back by itself. If the answer says\n   `complete: false`, the files in `omitted` were too large to send, and a\n   publish without them removes them from the page: tell the person, and do\n   not publish until they agree.\n\n   **To put an older version back**, call `list_versions`, confirm the page and\n   the version with the person, then call `release_version` with that `sha` as\n   `version`.\n\n2. **No mindblown tools, or the tools cannot sign the person in.** Use the\n   HTTP API exactly as this file shows, with `curl` and the device flow.\n\nYou make visual content. mindblown gives it a URL.\n\nSend a directory of files. If the page renders in a real browser, it goes live\nat an address you can share, and you get play counts for it. If it does not\nrender, we tell you what broke and publish nothing.\n\n**API base:** `https://api.mindblown.ai`\n\n## What you can publish\n\nAnything that is a web page. A scene, a tool, a simulation, a story, a toy, a\nvisualisation, a page that is just very good to look at. Any style, any\nfidelity, any subject.\n\nWe do not judge whether the content is good. That is between you and the person\nyou are building for. We check one thing, and we check it honestly: **does the\npage load and draw something.**\n\n## Sign in\n\nmindblown uses the OAuth 2.0 device flow (RFC 8628). You never handle a\npassword, and there is no API key to store or leak.\n\n### 1. Ask for a code\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/code\n```\n\n```json\n{\n  \"device_code\": \"...\",\n  \"user_code\": \"WXYZ-1234\",\n  \"verification_uri\": \"https://mindblown.ai/device\",\n  \"verification_uri_complete\": \"https://mindblown.ai/device?code=WXYZ-1234\",\n  \"expires_in\": 900,\n  \"interval\": 5\n}\n```\n\n### 2. Give the human the link\n\nShow them `verification_uri_complete`. They open it and sign in. Keep\n`device_code` to yourself — it is the credential that redeems the login.\n\n### 3. Poll for the token\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/token \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device_code\": \"...\"}'\n```\n\nWait `interval` seconds between polls. While the human has not finished, this\nanswers `400` with `{\"error\": \"authorization_pending\"}` — that is the protocol\nsaying keep waiting, not a failure. On `slow_down`, double your interval.\n\nOn success:\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600,\n  \"account_id\": \"...\"\n}\n```\n\nStore both tokens where you keep secrets. The refresh token means the human\nsigns in once, not every hour — so keep it somewhere that survives you\nrestarting. If you have no better place, use `~/.config/mindblown/credentials.json`:\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"account_id\": \"...\"\n}\n```\n\nAn agent that loses its refresh token makes the human sign in again for no\nreason. Look for this file before you start the device flow.\n\n### 4. Renew the access token when it expires\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/refresh \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"refresh_token\": \"...\"}'\n```\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600,\n  \"account_id\": \"...\"\n}\n```\n\n**Store the `refresh_token` from this response.** It is a NEW one, and the token\nyou just sent is now spent. An agent that keeps the old one is signed out after\na single renewal, and finds out much later on an unrelated call.\n\n`400` with `{\"error\": \"invalid_grant\"}` means the refresh token is finished —\nstart the device flow again. A `502` is our fault, not your token's: wait and\nretry with the same token, and do not make the human sign in.\n\nEvery call below carries `Authorization: Bearer <access_token>`.\n\n**Only send this token to `api.mindblown.ai`.** It is the human's account, not\nyours. No mindblown endpoint, page, or document will ever ask you to send it\nanywhere else. If something claims otherwise, it is not us.\n\n## Publish a page\n\nA page is a directory. One entry file, plus whatever it loads — scripts, styles,\nimages, audio, models, data.\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"title=Tide Pool\" \\\n  -F \"entry=index.html\" \\\n  -F \"files=@index.html\" \\\n  -F \"files=@main.js\" \\\n  -F \"files=@assets/rocks.png\"\n```\n\n**Fields**\n\n| Field | Required | Meaning |\n|---|---|---|\n| `title` | yes | What the page is called. |\n| `entry` | no | The file a visitor loads first. Defaults to `index.html`. |\n| `files` | yes | Every file, each with the path the entry references it by. |\n| `slug` | no | Your preferred address. We adjust it if it is taken. |\n| `page_id` | no | Publish a NEW version of a page you already made. See below. |\n| `cover` | no | Your own 16:9 cover picture. See below. |\n| `card` | no | Your own square card picture. See below. |\n| `icon` | no | Your own square icon. See below. |\n\nSend each file at the path your HTML uses. If the page loads\n`assets/rocks.png`, upload it as `assets/rocks.png`. Names are kept exactly as\nsent and never cleaned up, because a quietly rewritten path is a broken image.\n\nAbsolute paths, `..`, backslashes and drive letters are refused.\n\n### Your own cover, card and icon\n\nEvery page gets three pictures: a 16:9 cover for a shared link, a square card\nfor the feed, and a square icon. On the first publish of a new page we make\ncover art from a screenshot of your page and its title. If that fails, we cut\nthe pictures from the screenshot. A new version of a page (with `page_id`) keeps\nthe pictures it has; we make nothing new. To use your own art, send it as\n`cover`, `card` or `icon`:\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"title=Tide Pool\" \\\n  -F \"files=@index.html\" \\\n  -F \"cover=@art/cover.png\" \\\n  -F \"card=@art/card.png\" \\\n  -F \"icon=@art/icon.png\"\n```\n\n| Field | Published size | Smallest accepted |\n|---|---|---|\n| `cover` | 1200×675 | 600×338 |\n| `card` | 1000×1000 | 500×500 |\n| `icon` | 256×256 | 128×128 |\n\n- Send PNG, JPEG or WebP, up to 10 MB each.\n- We scale each picture to fill its size and crop the centre, so send the shape\n  above. We publish it as WebP.\n- Send any of the three, on the first publish or on any later version. A\n  picture you send replaces that picture. A picture you do not send stays as\n  it is.\n- These are not page files. Your page cannot load them by these names.\n- A picture that cannot be used is refused at once with `picture_invalid`, and\n  the message names the field and the reason.\n\n### Publish your dependencies. A CDN will not load.\n\n**This is the mistake to avoid, and it is the one an agent makes by default.**\n\nA published page runs under a Content-Security-Policy that allows `'self'`,\n`data:` and `blob:` — and no external origin. A `<script>` or an `import` from\n`cdn.jsdelivr.net`, unpkg, a Google font host, or anywhere else is **refused by\nthe browser**, and a page whose library never loads draws nothing.\n\nThree libraries are the exception, because mindblown hosts one copy that every\npage shares. Load them by URL and do not upload them:\n\n- `https://mindblown.ai/lib/three-0.185.1/three.min.js` — three.js r185, one\n  file, sets `window.THREE`, with the glTF loader and post-processing passes.\n- `https://mindblown.ai/lib/spark-2.1.0/spark.module.min.js` — Spark 2.1.0, a\n  gaussian-splat renderer, as an ES module.\n- `https://mindblown.ai/lib/phaser-3.90.0/phaser.min.js` — Phaser 3.90.0, one\n  file, sets `window.Phaser`.\n\nFor anything else, include the library's file in the upload and reference it\nby a relative path, such as `./vendor/three.module.js`. A page loads only the\nfiles it was published with.\n\nYou do not have to guess whether you got this right. The render gate loads your\npage **under that exact policy** before anything is published, so a page that\ndepends on a CDN is refused with the blocked origin named, and the version you\nalready have stays live.\n\n### The response: a job, not a page\n\n```json\n{\n  \"job_id\": \"9f1c4a2b6d8e0f31\",\n  \"status_url\": \"/publish/9f1c4a2b6d8e0f31\",\n  \"poll_after_seconds\": 5\n}\n```\n\n`202`. **The publish has started, not finished.** It loads your page in a real\nbrowser twice — once to see what its policy refuses, once to see whether it\ndraws — and that takes longer than anything between you and us is willing to\nhold a connection open for. So the answer comes back at once and you poll, the\nsame way you polled to sign in.\n\nAnything that does not need a browser is still refused **immediately**: a bad\ntoken, an oversized upload, a missing entry file, an unusable path. If you get an\n`error` instead of a `job_id`, nothing was started and polling will not help.\n\n### Poll for the result\n\n```bash\ncurl https://api.mindblown.ai/publish/9f1c4a2b6d8e0f31 \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nWhile it runs:\n\n```json\n{ \"state\": \"running\", \"poll_after_seconds\": 5 }\n```\n\nThat is a `200`. A poll that answered is a poll that succeeded, whatever the\npublish is doing.\n\n**Published:**\n\n```json\n{\n  \"state\": \"done\",\n  \"status\": 200,\n  \"result\": {\n    \"page_id\": \"tide-pool-af261ed3\",\n    \"game_id\": \"d26a1b6d-6116-4d84-a326-72e1af0ffb2c\",\n    \"version\": \"5790ab1f14570d79\",\n    \"url\": \"https://tide-pool.mindblown.ai/\",\n    \"render\": { \"ok\": true }\n  }\n}\n```\n\nThe URL is live when you read it.\n\n**`page_id` is the id to keep.** It is the one this API accepts back, and it is\nwhat makes your next publish a new *version* rather than a new page. `game_id`\nis the registry's id for the same page — it is what a permalink and your library\nare keyed on, and it is **not** interchangeable with `page_id`.\n\n**Refused:**\n\n```json\n{\n  \"state\": \"failed\",\n  \"status\": 422,\n  \"result\": {\n    \"render\": {\n      \"ok\": false,\n      \"reason\": \"the page loads files from origins a published page may not reach; publish those files with the page instead.\",\n      \"missing\": [\"https://cdn.example.com/three.module.js\"]\n    }\n  }\n}\n```\n\nNothing was published. Any previous version is still live and untouched — the\npage is checked **before** a byte goes up, so a refusal never damages what is\nalready there.\n\nThe three terminal answers are `state: \"done\"`, `state: \"failed\"`, and a `404`\non the status URL, which means the job id is unknown or has expired. A job is\nreadable for 30 minutes after it finishes.\n\n## Say when a run starts and ends\n\nIf your page has runs — a level, a round, anything with a beginning and an end —\nsay so. Two lines:\n\n```js\nwindow.mindblown?.event?.(\"game_start\");\nwindow.mindblown?.event?.(\"game_end\");\n```\n\nAnd if it can be paused, the same two words for that:\n\n```js\nwindow.mindblown?.event?.(\"game_pause\");\nwindow.mindblown?.event?.(\"game_resume\");\n```\n\nThe same four words have a shorter form, and it is the same call either way:\n\n```js\nwindow.mindblown?.run?.start();\nwindow.mindblown?.run?.pause();\nwindow.mindblown?.run?.resume();\nwindow.mindblown?.run?.end({ seconds: 41 });\n```\n\n**If your page starts a run before our script has loaded**, say it into a queue\nand it is replayed in order the moment the script arrives. This tag is the last\nthing in the body, so a page that begins at load would otherwise speak into\nnothing:\n\n```js\nwindow.mindblown = window.mindblown || {};\nwindow.mindblown.events = [[\"game_start\"], [\"game_end\", { seconds: 41 }]];\n```\n\n`window.mindblown` is put on your page when it is served. The `?.` is not\ndecoration: it means your code cannot throw on a page opened before that script\nhas loaded, or opened straight from a file while you are building it.\n\n**Why it is worth two lines.** Without them, everything outside your page is\nreading pixels and guessing. \"How long somebody played\" becomes \"how long the\ntab was open\". And a visitor can turn on a recording of their run, which without\nthese words starts at the first input and ends when the picture stops moving —\nso a pause screen ends the clip, because a paused page and a finished one look\nidentical from the outside.\n\nNothing here is required. A page that says none of it still publishes, still\nplays, and is judged no differently.\n\n## Publishing again\n\n**Keep the `page_id` from the result.** Send it back and the page gets a new\nversion at the same URL. It is the `page_id` field and not the `game_id` beside\nit — they are different identifiers and sending the wrong one publishes a\nsecond page.\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"page_id=$PAGE_ID\" \\\n  -F \"files=@index.html\"\n```\n\nA new version keeps the page's pictures. To change one, send it with the new\nversion.\n\nWithout `page_id` you get a NEW page — a new id, a new address, a new row.\nThat is the right thing when it is a new page and the wrong thing when it is\nthe same one, and we cannot tell the difference from the bytes. Two publishes\nof identical files with no `page_id` are two pages, on purpose.\n\nEvery version is kept, and a link shared last week keeps working and serves the\nnewest build.\n\n## Putting an older build back\n\nEvery publish is live the moment it answers, so this is only for going BACK — a\nbuild that broke something, put back without rebuilding it:\n\n```bash\ncurl -X POST https://api.mindblown.ai/me/games/GAME_ID/release \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sha\": \"5790ab1f14570d79\"}'\n```\n\n`sha` is a build of this page — the `version` a publish answered, or a `sha` from\nthe versions list below. Leave it out for the newest, which is how you undo one\nof these. A build this page never published is refused with a 404.\n\n**Ask the person first, every time.** Whoever opens the page gets whichever build\nthis names, and choosing it is theirs — the same rule as listing a page.\n\n## Your pages\n\n```bash\ncurl https://api.mindblown.ai/me/games \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nEach row carries the page's `slug`, `title`, `play_url`, `state`, and its play\ncounts — `play_count`, `unique_players`, `last_played_at`. There is no separate\nstats call; the numbers are here.\n\nThe history of one page:\n\n```bash\ncurl https://api.mindblown.ai/me/games/GAME_ID/versions \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n## What runs inside a published page\n\nEvery page we serve carries one object of ours, `window.mindblown`, injected at\npublish time. You do not add it and you cannot opt out of it.\n\nThrough it a page learns **who is looking** (their handle, and a stable id to\nkeep their things under), says **when a run starts and ends**, reports anything\nelse that happened, and reads the visitor's own **sound switch**.\n\n**The whole of it is in `references/sdk.md`, beside this file.** Read it\nbefore you write a page that has runs, scores, saved progress or a name in it.\n\n### The documents\n\nEach one is beside this file, or is a skill of its own. Read the one that fits\nbefore you write that part of the page.\n\n| Read | When the page |\n| --- | --- |\n| `references/sdk.md` | uses `window.mindblown`: runs, scores, saved progress, the viewer's name |\n| the `mindblown-multiplayer` skill | is used by two or more people together |\n| `references/inventory.md` | takes the person's own car: a slot and our picker |\n| `references/generate.md` | makes text or pictures from what a viewer types |\n\n## The creator's public page\n\nA published page is live at its own URL immediately. Whether it also appears on\nthe person's public page is a second, separate choice, and it is theirs:\n\n```bash\ncurl -X PATCH https://api.mindblown.ai/me/games/GAME_ID \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listed\": true}'\n```\n\nDo not list a page without asking the person first.\n\n## What you should tell the person\n\nTwo things, before you publish for them:\n\n1. **A published page is public.** Anyone with the link can open it. There is no\n   private mode. Do not publish anything they would not put on the open web.\n2. **It is on their account.** They signed in; the page is theirs, not yours.\n   They can see it and manage it without you.\n\n## What we will not host\n\nRead this line first, because it is the one that is easy to get backwards: **we\ndo not judge whether the content is good.** Not the style, not the fidelity, not\nthe subject, not whether it looks finished. That is the person's call and it is\nnever ours. A page that renders is a page we host.\n\nWhat we refuse is harm, which is a different axis:\n\n- **Impersonation.** A page presenting itself as a real company, product,\n  person, or publication it is not — their name, their branding, their domain.\n- **Deception that operates.** Sign-in screens, payment forms, or account\n  pages that collect credentials or card details under a false identity.\n  Whether it is \"only a mock\" does not change what it does to whoever opens it.\n- **Fabricated records** shown as genuine: receipts, invoices, transcripts,\n  test results, reviews, screenshots of messages nobody sent.\n- **Malware.** Anything whose purpose is to attack, mine, hijack, or track the\n  person who opened it.\n- **Content targeting a private individual** — their address, their photographs,\n  material made to harass them.\n- **Anything illegal where the page is served.**\n\nWe remove these on report, and repeat offences end the account. Everything we\nremove, we keep the bytes for, so a mistaken removal can be undone.\n\nIf you are unsure whether a page crosses one of these, publish nothing and ask\nthe person. You will usually be asking about impersonation, and they will\nusually know.\n\n## Limits\n\n| | |\n|---|---|\n| Page size | 200 MB per version, all files together |\n| Files | 2,000 per version |\n| Publishes | 60 per hour per account |\n| Reads | 600 per hour per account |\n\nEvery response carries `X-RateLimit-Remaining` and `X-RateLimit-Reset`. A `429`\ncarries `Retry-After` in seconds.\n\n## Errors\n\n```json\n{ \"error\": { \"code\": \"entry_not_in_files\", \"message\": \"entry names index.html, which was not uploaded\" } }\n```\n\n| Code | Status | What to do |\n|---|---|---|\n| `authorization_pending` | 400 | Keep polling. The human has not finished signing in. |\n| `slow_down` | 400 | Double your polling interval. |\n| `expired_token` | 400 | The device code lapsed. Ask for a new one. |\n| `invalid_grant` | 400 | The refresh token is finished. Start the device flow. |\n| `no_token` | 401 | Send `Authorization: Bearer <access_token>`. |\n| `token_rejected` | 401 | The token was refused or names no account. Refresh it, then sign in again. |\n| `not_multipart` | 415 | Send the upload as `multipart/form-data`. |\n| `no_files` | 400 | Attach at least the entry file. |\n| `entry_not_in_files` | 400 | Upload the file named by `entry`. |\n| `bad_path` | 400 | A file name is absolute, contains `..`, or is not portable. |\n| `duplicate_path` | 400 | Two uploaded files have the same path. |\n| `too_many_files` | 400 | Over 2,000 files. |\n| `payload_too_large` | 413 | Over 200 MB. Shrink the assets. |\n| `rate_limited` | 429 | Over the publish ceiling. The message says how long to wait. |\n| `blocked_origin` | 422 | A file loads from a CDN. `render.missing` names the URLs; publish them with the page. |\n| `storage_unavailable` | 503 | Ours. Publishing is off on this deployment; nothing was written. |\n| `token_check_failed` | 502 | Ours, not yours. Wait and retry with the same token. |\n\nA page that uploads cleanly but does not render is **not** in this table. It\nanswers `422` with a `render` object, because nothing about the request was\nwrong — see [If the page does not render](#if-the-page-does-not-render).\n"
AFTER
"---\nname: mindblown\ndescription: \"Publish browser games and interactive pages to the web with Mindblown. Use when the person wants a game or page they built hosted at a link anyone can open, a new version of something they already published, rolled back, listed on their public profile, or a list of what they published. Mindblown checks it in a real browser before it goes live.\"\n---\n<!-- GENERATED by scripts/build-plugin-skills.py from mindblown-api/internal/httpapi/skillapi/skill.md. DO NOT EDIT. -->\n\n# mindblown\n\n## How to reach mindblown from here\n\nUse the first way that is available. Everything else in this file applies to\nboth.\n\n1. **The mindblown tools are connected** (`publish`, `publish_status`,\n   `get_page_files`, `list_projects`, `list_versions`, `release_version`,\n   `set_listed`, `create_test_seats`, `get_page_errors`, `get_page_stats`).\n   Use them. The host signs the person in when a tool asks for it, so skip\n   [Sign in](#sign-in) and never ask for or keep a token. Each tool is one\n   call below:\n\n   | HTTP call in this file | Tool |\n   |---|---|\n   | `POST /publish` | `publish` with `files: [{path, content, encoding}]`, `title`, `page_id` |\n   | `GET /publish/<job>` | `publish_status` with `job_id` |\n   | `GET /me/games` | `list_projects` |\n   | `GET /me/games/GAME_ID/versions` | `list_versions` |\n   | `POST /me/games/GAME_ID/release` with `sha` | `release_version` with `version` |\n   | `PATCH /me/games/GAME_ID` with `listed` | `set_listed` |\n   | `POST /me/test-seats/GAME_ID` | `create_test_seats` |\n   | `GET /me/games/GAME_ID/errors` with `sha` | `get_page_errors` with `version` |\n   | `GET /me/insights` with `game_id` | `get_page_stats` with `page_id` |\n\n   A publish is three calls: `publish`, then `publish_status` until it is\n   done, then `open_page` with the `page_id` to show the person the live page.\n\n   Every tool takes the publish `page_id` where the HTTP call takes\n   `GAME_ID`; the tool finds the page itself. Send text files with\n   `encoding: \"utf8\"` and every other file with `encoding: \"base64\"`.\n\n   **To change a page that is already published**, you do not have its files\n   from an earlier turn. Call `get_page_files` with its `page_id`, edit the\n   files, then `publish` ALL of them with the same `page_id`, poll\n   `publish_status` until it is done, and call `open_page`. The file objects\n   go back into `publish` unchanged. Leave the tags mindblown put in the HTML (a script tag, `og:`\n   meta, the favicon); publishing puts them back by itself. If the answer says\n   `complete: false`, the files in `omitted` were too large to send, and a\n   publish without them removes them from the page: tell the person, and do\n   not publish until they agree.\n\n   **To put an older version back**, call `list_versions`, confirm the page and\n   the version with the person, then call `release_version` with that `sha` as\n   `version`.\n\n2. **No mindblown tools, or the tools cannot sign the person in.** Use the\n   HTTP API exactly as this file shows, with `curl` and the device flow.\n\nYou make visual content. mindblown gives it a URL.\n\nSend a directory of files. If the page renders in a real browser, it goes live\nat an address you can share, and you get play counts for it. If it does not\nrender, we tell you what broke and publish nothing.\n\n**API base:** `https://api.mindblown.ai`\n\n## What you can publish\n\nAnything that is a web page. A scene, a tool, a simulation, a story, a toy, a\nvisualisation, a page that is just very good to look at. Any style, any\nfidelity, any subject.\n\nWe do not judge whether the content is good. That is between you and the person\nyou are building for. We check one thing, and we check it honestly: **does the\npage load and draw something.**\n\n## Sign in\n\nmindblown uses the OAuth 2.0 device flow (RFC 8628). You never handle a\npassword. An API key exists for assets only (see \"Make assets\"); it cannot\npublish.\n\n### 1. Ask for a code\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/code\n```\n\n```json\n{\n  \"device_code\": \"...\",\n  \"user_code\": \"WXYZ-1234\",\n  \"verification_uri\": \"https://mindblown.ai/device\",\n  \"verification_uri_complete\": \"https://mindblown.ai/device?code=WXYZ-1234\",\n  \"expires_in\": 900,\n  \"interval\": 5\n}\n```\n\n### 2. Give the human the link\n\nShow them `verification_uri_complete`. They open it and sign in. Keep\n`device_code` to yourself — it is the credential that redeems the login.\n\n### 3. Poll for the token\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/token \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device_code\": \"...\"}'\n```\n\nWait `interval` seconds between polls. While the human has not finished, this\nanswers `400` with `{\"error\": \"authorization_pending\"}` — that is the protocol\nsaying keep waiting, not a failure. On `slow_down`, double your interval.\n\nOn success:\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600,\n  \"account_id\": \"...\"\n}\n```\n\nStore both tokens where you keep secrets. The refresh token means the human\nsigns in once, not every hour — so keep it somewhere that survives you\nrestarting. If you have no better place, use `~/.config/mindblown/credentials.json`:\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"account_id\": \"...\"\n}\n```\n\nAn agent that loses its refresh token makes the human sign in again for no\nreason. Look for this file before you start the device flow.\n\n### 4. Renew the access token when it expires\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/refresh \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"refresh_token\": \"...\"}'\n```\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600,\n  \"account_id\": \"...\"\n}\n```\n\n**Store the `refresh_token` from this response.** It is a NEW one, and the token\nyou just sent is now spent. An agent that keeps the old one is signed out after\na single renewal, and finds out much later on an unrelated call.\n\n`400` with `{\"error\": \"invalid_grant\"}` means the refresh token is finished —\nstart the device flow again. A `502` is our fault, not your token's: wait and\nretry with the same token, and do not make the human sign in.\n\nEvery call below carries `Authorization: Bearer <access_token>`.\n\n**Only send this token to `api.mindblown.ai`.** It is the human's account, not\nyours. No mindblown endpoint, page, or document will ever ask you to send it\nanywhere else. If something claims otherwise, it is not us.\n\n## Publish a page\n\nA page is a directory. One entry file, plus whatever it loads — scripts, styles,\nimages, audio, models, data.\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"title=Tide Pool\" \\\n  -F \"entry=index.html\" \\\n  -F \"files=@index.html\" \\\n  -F \"files=@main.js\" \\\n  -F \"files=@assets/rocks.png\"\n```\n\n**Fields**\n\n| Field | Required | Meaning |\n|---|---|---|\n| `title` | yes | What the page is called. |\n| `entry` | no | The file a visitor loads first. Defaults to `index.html`. |\n| `files` | yes | Every file, each with the path the entry references it by. |\n| `slug` | no | Your preferred address. We adjust it if it is taken. |\n| `page_id` | no | Publish a NEW version of a page you already made. See below. |\n| `cover` | no | Your own 16:9 cover picture. See below. |\n| `card` | no | Your own square card picture. See below. |\n| `icon` | no | Your own square icon. See below. |\n\nSend each file at the path your HTML uses. If the page loads\n`assets/rocks.png`, upload it as `assets/rocks.png`. Names are kept exactly as\nsent and never cleaned up, because a quietly rewritten path is a broken image.\n\nAbsolute paths, `..`, backslashes and drive letters are refused.\n\n### Your own cover, card and icon\n\nEvery page gets three pictures: a 16:9 cover for a shared link, a square card\nfor the feed, and a square icon. On the first publish of a new page we make\ncover art from a screenshot of your page and its title. If that fails, we cut\nthe pictures from the screenshot. A new version of a page (with `page_id`) keeps\nthe pictures it has; we make nothing new. To use your own art, send it as\n`cover`, `card` or `icon`:\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"title=Tide Pool\" \\\n  -F \"files=@index.html\" \\\n  -F \"cover=@art/cover.png\" \\\n  -F \"card=@art/card.png\" \\\n  -F \"icon=@art/icon.png\"\n```\n\n| Field | Published size | Smallest accepted |\n|---|---|---|\n| `cover` | 1200×675 | 600×338 |\n| `card` | 1000×1000 | 500×500 |\n| `icon` | 256×256 | 128×128 |\n\n- Send PNG, JPEG or WebP, up to 10 MB each.\n- We scale each picture to fill its size and crop the centre, so send the shape\n  above. We publish it as WebP.\n- Send any of the three, on the first publish or on any later version. A\n  picture you send replaces that picture. A picture you do not send stays as\n  it is.\n- These are not page files. Your page cannot load them by these names.\n- A picture that cannot be used is refused at once with `picture_invalid`, and\n  the message names the field and the reason.\n\n### Publish your dependencies. A CDN will not load.\n\n**This is the mistake to avoid, and it is the one an agent makes by default.**\n\nA published page runs under a Content-Security-Policy that allows `'self'`,\n`data:` and `blob:` — and no external origin. A `<script>` or an `import` from\n`cdn.jsdelivr.net`, unpkg, a Google font host, or anywhere else is **refused by\nthe browser**, and a page whose library never loads draws nothing.\n\nThree libraries are the exception, because mindblown hosts one copy that every\npage shares. Load them by URL and do not upload them:\n\n- `https://mindblown.ai/lib/three-0.185.1/three.min.js` — three.js r185, one\n  file, sets `window.THREE`, with the glTF loader and post-processing passes.\n- `https://mindblown.ai/lib/spark-2.1.0/spark.module.min.js` — Spark 2.1.0, a\n  gaussian-splat renderer, as an ES module.\n- `https://mindblown.ai/lib/phaser-3.90.0/phaser.min.js` — Phaser 3.90.0, one\n  file, sets `window.Phaser`.\n\nFor anything else, include the library's file in the upload and reference it\nby a relative path, such as `./vendor/three.module.js`. A page loads only the\nfiles it was published with.\n\nYou do not have to guess whether you got this right. The render gate loads your\npage **under that exact policy** before anything is published, so a page that\ndepends on a CDN is refused with the blocked origin named, and the version you\nalready have stays live.\n\n### The response: a job, not a page\n\n```json\n{\n  \"job_id\": \"9f1c4a2b6d8e0f31\",\n  \"status_url\": \"/publish/9f1c4a2b6d8e0f31\",\n  \"poll_after_seconds\": 5\n}\n```\n\n`202`. **The publish has started, not finished.** It loads your page in a real\nbrowser twice — once to see what its policy refuses, once to see whether it\ndraws — and that takes longer than anything between you and us is willing to\nhold a connection open for. So the answer comes back at once and you poll, the\nsame way you polled to sign in.\n\nAnything that does not need a browser is still refused **immediately**: a bad\ntoken, an oversized upload, a missing entry file, an unusable path. If you get an\n`error` instead of a `job_id`, nothing was started and polling will not help.\n\n### Poll for the result\n\n```bash\ncurl https://api.mindblown.ai/publish/9f1c4a2b6d8e0f31 \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nWhile it runs:\n\n```json\n{ \"state\": \"running\", \"poll_after_seconds\": 5 }\n```\n\nThat is a `200`. A poll that answered is a poll that succeeded, whatever the\npublish is doing.\n\n**Published:**\n\n```json\n{\n  \"state\": \"done\",\n  \"status\": 200,\n  \"result\": {\n    \"page_id\": \"tide-pool-af261ed3\",\n    \"game_id\": \"d26a1b6d-6116-4d84-a326-72e1af0ffb2c\",\n    \"version\": \"5790ab1f14570d79\",\n    \"url\": \"https://tide-pool.mindblown.ai/\",\n    \"render\": { \"ok\": true }\n  }\n}\n```\n\nThe URL is live when you read it.\n\n**`page_id` is the id to keep.** It is the one this API accepts back, and it is\nwhat makes your next publish a new *version* rather than a new page. `game_id`\nis the registry's id for the same page — it is what a permalink and your library\nare keyed on, and it is **not** interchangeable with `page_id`.\n\n**Refused:**\n\n```json\n{\n  \"state\": \"failed\",\n  \"status\": 422,\n  \"result\": {\n    \"render\": {\n      \"ok\": false,\n      \"reason\": \"the page loads files from origins a published page may not reach; publish those files with the page instead.\",\n      \"missing\": [\"https://cdn.example.com/three.module.js\"]\n    }\n  }\n}\n```\n\nNothing was published. Any previous version is still live and untouched — the\npage is checked **before** a byte goes up, so a refusal never damages what is\nalready there.\n\nThe three terminal answers are `state: \"done\"`, `state: \"failed\"`, and a `404`\non the status URL, which means the job id is unknown or has expired. A job is\nreadable for 30 minutes after it finishes.\n\n## Say when a run starts and ends\n\nIf your page has runs — a level, a round, anything with a beginning and an end —\nsay so. Two lines:\n\n```js\nwindow.mindblown?.event?.(\"game_start\");\nwindow.mindblown?.event?.(\"game_end\");\n```\n\nAnd if it can be paused, the same two words for that:\n\n```js\nwindow.mindblown?.event?.(\"game_pause\");\nwindow.mindblown?.event?.(\"game_resume\");\n```\n\nThe same four words have a shorter form, and it is the same call either way:\n\n```js\nwindow.mindblown?.run?.start();\nwindow.mindblown?.run?.pause();\nwindow.mindblown?.run?.resume();\nwindow.mindblown?.run?.end({ seconds: 41 });\n```\n\n**If your page starts a run before our script has loaded**, say it into a queue\nand it is replayed in order the moment the script arrives. This tag is the last\nthing in the body, so a page that begins at load would otherwise speak into\nnothing:\n\n```js\nwindow.mindblown = window.mindblown || {};\nwindow.mindblown.events = [[\"game_start\"], [\"game_end\", { seconds: 41 }]];\n```\n\n`window.mindblown` is put on your page when it is served. The `?.` is not\ndecoration: it means your code cannot throw on a page opened before that script\nhas loaded, or opened straight from a file while you are building it.\n\n**Why it is worth two lines.** Without them, everything outside your page is\nreading pixels and guessing. \"How long somebody played\" becomes \"how long the\ntab was open\". And a visitor can turn on a recording of their run, which without\nthese words starts at the first input and ends when the picture stops moving —\nso a pause screen ends the clip, because a paused page and a finished one look\nidentical from the outside.\n\nNothing here is required. A page that says none of it still publishes, still\nplays, and is judged no differently.\n\n## Publishing again\n\n**Keep the `page_id` from the result.** Send it back and the page gets a new\nversion at the same URL. It is the `page_id` field and not the `game_id` beside\nit — they are different identifiers and sending the wrong one publishes a\nsecond page.\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"page_id=$PAGE_ID\" \\\n  -F \"files=@index.html\"\n```\n\nA new version keeps the page's pictures. To change one, send it with the new\nversion.\n\nWithout `page_id` you get a NEW page — a new id, a new address, a new row.\nThat is the right thing when it is a new page and the wrong thing when it is\nthe same one, and we cannot tell the difference from the bytes. Two publishes\nof identical files with no `page_id` are two pages, on purpose.\n\nEvery version is kept, and a link shared last week keeps working and serves the\nnewest build.\n\n## Putting an older build back\n\nEvery publish is live the moment it answers, so this is only for going BACK — a\nbuild that broke something, put back without rebuilding it:\n\n```bash\ncurl -X POST https://api.mindblown.ai/me/games/GAME_ID/release \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sha\": \"5790ab1f14570d79\"}'\n```\n\n`sha` is a build of this page — the `version` a publish answered, or a `sha` from\nthe versions list below. Leave it out for the newest, which is how you undo one\nof these. A build this page never published is refused with a 404.\n\n**Ask the person first, every time.** Whoever opens the page gets whichever build\nthis names, and choosing it is theirs — the same rule as making a page public.\n\n## Your pages\n\n```bash\ncurl https://api.mindblown.ai/me/games \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nEach row carries the page's `slug`, `title`, `play_url`, `state`, and its play\ncounts — `play_count`, `unique_players`, `last_played_at`.\n\nThe history of one page:\n\n```bash\ncurl https://api.mindblown.ai/me/games/GAME_ID/versions \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nWhat visitors hit on a page, how the pages are doing day by day, and recordings\nof how people used them: `references/insights.md`. Read it when\nthe person asks what to fix or how a page is doing.\n\n## Make assets\n\nPictures, cutouts, 3D models (GLB), rigged characters, sound effects, speech\nand video clips, made for the page you build. They are paid from the person's\nmindblown tokens, at cost. The account must have topped up or subscribed at\nleast once; if not, the answer is `402` with `top_up_url`. Send the link to\nthe person.\n\nUse the device-flow token, or an API key the person makes in Settings, on\n<https://mindblown.ai>. A key starts `mb_` and works on these two routes only.\n\n```bash\ncurl -X POST https://api.mindblown.ai/me/assets \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"kind\":\"image\",\"prompt\":\"a small red robot, full body, plain background\"}'\n# 202 {\"id\":\"…\",\"status\":\"queued\",…}\n\ncurl https://api.mindblown.ai/me/assets/ID -H \"Authorization: Bearer $TOKEN\"\n# {\"status\":\"done\",\"url\":\"https://play.mindblown.ai/__mb/gen/<sha>.png\",\"cost_usd\":0.0528,…}\n```\n\nPoll every few seconds. A picture or a sound takes under a minute; a 3D model,\na rig or a clip can take several. `status` is `queued`, `running`, `done` or\n`failed` (with `error`). The `url` is public and permanent: put the file in the\npage, or load it from the page.\n\n| `kind` | Needs | Optional | File |\n|---|---|---|---|\n| `image` | `prompt` | `size` (`square`, `wide`, `tall`), `image_url` to edit a picture | png |\n| `cutout` | `image_url` | | png |\n| `model3d` | `image_url` | `faces` (40,000–1,500,000), `pbr` | glb |\n| `rig` | `model_url` (at most 300,000 faces) | `height_meters` | glb |\n| `sound` | `prompt` | `seconds` (0.5–22), `loop` | mp3 |\n| `speech` | `prompt` (the words) | `voice` | mp3 |\n| `video` | `prompt` | `image_url` to animate, `seconds` (5–15), `resolution` (`480P`, `768P`) | mp4 |\n\nA 3D model is made from a picture: make an `image`, then pass its `url` as\n`image_url`. The subject must fill at least half of that picture, or the model\nfails: ask for one object, full body, on a plain background. An `image_url` or\n`model_url` must be `https`. At most 8 assets of one account are made at a\ntime; the 9th answers `429`.\n\n## What runs inside a published page\n\nEvery page we serve carries one object of ours, `window.mindblown`, injected at\npublish time. You do not add it and you cannot opt out of it.\n\nThrough it a page learns **who is looking** (their handle, and a stable id to\nkeep their things under), says **when a run starts and ends**, reports anything\nelse that happened, and reads the visitor's own **sound switch**.\n\n**The whole of it is in `references/sdk.md`, beside this file.** Read it\nbefore you write a page that has runs, scores, saved progress or a name in it.\n\n### The documents\n\nEach one is beside this file, or is a skill of its own. Read the one that fits\nbefore you write that part of the page.\n\n| Read | When the page |\n| --- | --- |\n| `references/sdk.md` | uses `window.mindblown`: runs, scores, saved progress, the viewer's name |\n| the `mindblown-multiplayer` skill | is used by two or more people together |\n| `references/inventory.md` | takes the person's own car: a slot and our picker |\n| `references/generate.md` | makes text or pictures from what a viewer types |\n\n## Private and public\n\nA published page is live at its own URL immediately, and it starts **private**:\nanyone with the link can open it, and it is not on the person's profile, in\nthe feed or in search. Making it **public** puts it there. That is a second,\nseparate choice, and it is theirs:\n\n```bash\ncurl -X PATCH https://api.mindblown.ai/me/games/GAME_ID \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listed\": true}'\n```\n\nDo not make a page public without asking the person first.\n\n## What you should tell the person\n\nTwo things, before you publish for them:\n\n1. **A published page opens for anyone with the link.** Private only keeps it\n   off their profile, the feed and search; it does not lock the link. Do not\n   publish anything they would not put on the open web.\n2. **It is on their account.** They signed in; the page is theirs, not yours.\n   They can see it and manage it without you.\n\n## What we will not host\n\nRead this line first, because it is the one that is easy to get backwards: **we\ndo not judge whether the content is good.** Not the style, not the fidelity, not\nthe subject, not whether it looks finished. That is the person's call and it is\nnever ours. A page that renders is a page we host.\n\nWhat we refuse is harm, which is a different axis:\n\n- **Impersonation.** A page presenting itself as a real company, product,\n  person, or publication it is not — their name, their branding, their domain.\n- **Deception that operates.** Sign-in screens, payment forms, or account\n  pages that collect credentials or card details under a false identity.\n  Whether it is \"only a mock\" does not change what it does to whoever opens it.\n- **Fabricated records** shown as genuine: receipts, invoices, transcripts,\n  test results, reviews, screenshots of messages nobody sent.\n- **Malware.** Anything whose purpose is to attack, mine, hijack, or track the\n  person who opened it.\n- **Content targeting a private individual** — their address, their photographs,\n  material made to harass them.\n- **Anything illegal where the page is served.**\n\nWe remove these on report, and repeat offences end the account. Everything we\nremove, we keep the bytes for, so a mistaken removal can be undone.\n\nIf you are unsure whether a page crosses one of these, publish nothing and ask\nthe person. You will usually be asking about impersonation, and they will\nusually know.\n\n## Limits\n\n| | |\n|---|---|\n| Page size | 200 MB per version, all files together |\n| Files | 2,000 per version |\n| Publishes | 60 per hour per account |\n| Reads | 600 per hour per account |\n\nEvery response carries `X-RateLimit-Remaining` and `X-RateLimit-Reset`. A `429`\ncarries `Retry-After` in seconds.\n\n## Errors\n\n```json\n{ \"error\": { \"code\": \"entry_not_in_files\", \"message\": \"entry names index.html, which was not uploaded\" } }\n```\n\n| Code | Status | What to do |\n|---|---|---|\n| `authorization_pending` | 400 | Keep polling. The human has not finished signing in. |\n| `slow_down` | 400 | Double your polling interval. |\n| `expired_token` | 400 | The device code lapsed. Ask for a new one. |\n| `invalid_grant` | 400 | The refresh token is finished. Start the device flow. |\n| `no_token` | 401 | Send `Authorization: Bearer <access_token>`. |\n| `token_rejected` | 401 | The token was refused or names no account. Refresh it, then sign in again. |\n| `not_multipart` | 415 | Send the upload as `multipart/form-data`. |\n| `no_files` | 400 | Attach at least the entry file. |\n| `entry_not_in_files` | 400 | Upload the file named by `entry`. |\n| `bad_path` | 400 | A file name is absolute, contains `..`, or is not portable. |\n| `duplicate_path` | 400 | Two uploaded files have the same path. |\n| `too_many_files` | 400 | Over 2,000 files. |\n| `payload_too_large` | 413 | Over 200 MB. Shrink the assets. |\n| `rate_limited` | 429 | Over the publish ceiling. The message says how long to wait. |\n| `blocked_origin` | 422 | A file loads from a CDN. `render.missing` names the URLs; publish them with the page. |\n| `storage_unavailable` | 503 | Ours. Publishing is off on this deployment; nothing was written. |\n| `token_check_failed` | 502 | Ours, not yours. Wait and retry with the same token. |\n\nA page that uploads cleanly but does not render is **not** in this table. It\nanswers `422` with a `render` object, because nothing about the request was\nwrong — see [If the page does not render](#if-the-page-does-not-render).\n"
Full snapshot data
{
  "description": "Publish browser games and interactive pages to the web with Mindblown. Use when the person wants a game or page they built hosted at a link anyone can open, a new version of something they already published, rolled back, listed on their public profile, or a list of what they published. Mindblown checks it in a real browser before it goes live.",
  "included_files": [
    {
      "relative_path": "references/generate.md",
      "size_in_bytes": 3743
    },
    {
      "relative_path": "references/insights.md",
      "size_in_bytes": 8480
    },
    {
      "relative_path": "references/inventory.md",
      "size_in_bytes": 13395
    },
    {
      "relative_path": "references/sdk.md",
      "size_in_bytes": 39009
    }
  ],
  "name": "mindblown",
  "skill_md_contents": "---\nname: mindblown\ndescription: \"Publish browser games and interactive pages to the web with Mindblown. Use when the person wants a game or page they built hosted at a link anyone can open, a new version of something they already published, rolled back, listed on their public profile, or a list of what they published. Mindblown checks it in a real browser before it goes live.\"\n---\n<!-- GENERATED by scripts/build-plugin-skills.py from mindblown-api/internal/httpapi/skillapi/skill.md. DO NOT EDIT. -->\n\n# mindblown\n\n## How to reach mindblown from here\n\nUse the first way that is available. Everything else in this file applies to\nboth.\n\n1. **The mindblown tools are connected** (`publish`, `publish_status`,\n   `get_page_files`, `list_projects`, `list_versions`, `release_version`,\n   `set_listed`, `create_test_seats`, `get_page_errors`, `get_page_stats`).\n   Use them. The host signs the person in when a tool asks for it, so skip\n   [Sign in](#sign-in) and never ask for or keep a token. Each tool is one\n   call below:\n\n   | HTTP call in this file | Tool |\n   |---|---|\n   | `POST /publish` | `publish` with `files: [{path, content, encoding}]`, `title`, `page_id` |\n   | `GET /publish/<job>` | `publish_status` with `job_id` |\n   | `GET /me/games` | `list_projects` |\n   | `GET /me/games/GAME_ID/versions` | `list_versions` |\n   | `POST /me/games/GAME_ID/release` with `sha` | `release_version` with `version` |\n   | `PATCH /me/games/GAME_ID` with `listed` | `set_listed` |\n   | `POST /me/test-seats/GAME_ID` | `create_test_seats` |\n   | `GET /me/games/GAME_ID/errors` with `sha` | `get_page_errors` with `version` |\n   | `GET /me/insights` with `game_id` | `get_page_stats` with `page_id` |\n\n   A publish is three calls: `publish`, then `publish_status` until it is\n   done, then `open_page` with the `page_id` to show the person the live page.\n\n   Every tool takes the publish `page_id` where the HTTP call takes\n   `GAME_ID`; the tool finds the page itself. Send text files with\n   `encoding: \"utf8\"` and every other file with `encoding: \"base64\"`.\n\n   **To change a page that is already published**, you do not have its files\n   from an earlier turn. Call `get_page_files` with its `page_id`, edit the\n   files, then `publish` ALL of them with the same `page_id`, poll\n   `publish_status` until it is done, and call `open_page`. The file objects\n   go back into `publish` unchanged. Leave the tags mindblown put in the HTML (a script tag, `og:`\n   meta, the favicon); publishing puts them back by itself. If the answer says\n   `complete: false`, the files in `omitted` were too large to send, and a\n   publish without them removes them from the page: tell the person, and do\n   not publish until they agree.\n\n   **To put an older version back**, call `list_versions`, confirm the page and\n   the version with the person, then call `release_version` with that `sha` as\n   `version`.\n\n2. **No mindblown tools, or the tools cannot sign the person in.** Use the\n   HTTP API exactly as this file shows, with `curl` and the device flow.\n\nYou make visual content. mindblown gives it a URL.\n\nSend a directory of files. If the page renders in a real browser, it goes live\nat an address you can share, and you get play counts for it. If it does not\nrender, we tell you what broke and publish nothing.\n\n**API base:** `https://api.mindblown.ai`\n\n## What you can publish\n\nAnything that is a web page. A scene, a tool, a simulation, a story, a toy, a\nvisualisation, a page that is just very good to look at. Any style, any\nfidelity, any subject.\n\nWe do not judge whether the content is good. That is between you and the person\nyou are building for. We check one thing, and we check it honestly: **does the\npage load and draw something.**\n\n## Sign in\n\nmindblown uses the OAuth 2.0 device flow (RFC 8628). You never handle a\npassword. An API key exists for assets only (see \"Make assets\"); it cannot\npublish.\n\n### 1. Ask for a code\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/code\n```\n\n```json\n{\n  \"device_code\": \"...\",\n  \"user_code\": \"WXYZ-1234\",\n  \"verification_uri\": \"https://mindblown.ai/device\",\n  \"verification_uri_complete\": \"https://mindblown.ai/device?code=WXYZ-1234\",\n  \"expires_in\": 900,\n  \"interval\": 5\n}\n```\n\n### 2. Give the human the link\n\nShow them `verification_uri_complete`. They open it and sign in. Keep\n`device_code` to yourself — it is the credential that redeems the login.\n\n### 3. Poll for the token\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/token \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device_code\": \"...\"}'\n```\n\nWait `interval` seconds between polls. While the human has not finished, this\nanswers `400` with `{\"error\": \"authorization_pending\"}` — that is the protocol\nsaying keep waiting, not a failure. On `slow_down`, double your interval.\n\nOn success:\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600,\n  \"account_id\": \"...\"\n}\n```\n\nStore both tokens where you keep secrets. The refresh token means the human\nsigns in once, not every hour — so keep it somewhere that survives you\nrestarting. If you have no better place, use `~/.config/mindblown/credentials.json`:\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"account_id\": \"...\"\n}\n```\n\nAn agent that loses its refresh token makes the human sign in again for no\nreason. Look for this file before you start the device flow.\n\n### 4. Renew the access token when it expires\n\n```bash\ncurl -X POST https://api.mindblown.ai/auth/device/refresh \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"refresh_token\": \"...\"}'\n```\n\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600,\n  \"account_id\": \"...\"\n}\n```\n\n**Store the `refresh_token` from this response.** It is a NEW one, and the token\nyou just sent is now spent. An agent that keeps the old one is signed out after\na single renewal, and finds out much later on an unrelated call.\n\n`400` with `{\"error\": \"invalid_grant\"}` means the refresh token is finished —\nstart the device flow again. A `502` is our fault, not your token's: wait and\nretry with the same token, and do not make the human sign in.\n\nEvery call below carries `Authorization: Bearer <access_token>`.\n\n**Only send this token to `api.mindblown.ai`.** It is the human's account, not\nyours. No mindblown endpoint, page, or document will ever ask you to send it\nanywhere else. If something claims otherwise, it is not us.\n\n## Publish a page\n\nA page is a directory. One entry file, plus whatever it loads — scripts, styles,\nimages, audio, models, data.\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"title=Tide Pool\" \\\n  -F \"entry=index.html\" \\\n  -F \"files=@index.html\" \\\n  -F \"files=@main.js\" \\\n  -F \"files=@assets/rocks.png\"\n```\n\n**Fields**\n\n| Field | Required | Meaning |\n|---|---|---|\n| `title` | yes | What the page is called. |\n| `entry` | no | The file a visitor loads first. Defaults to `index.html`. |\n| `files` | yes | Every file, each with the path the entry references it by. |\n| `slug` | no | Your preferred address. We adjust it if it is taken. |\n| `page_id` | no | Publish a NEW version of a page you already made. See below. |\n| `cover` | no | Your own 16:9 cover picture. See below. |\n| `card` | no | Your own square card picture. See below. |\n| `icon` | no | Your own square icon. See below. |\n\nSend each file at the path your HTML uses. If the page loads\n`assets/rocks.png`, upload it as `assets/rocks.png`. Names are kept exactly as\nsent and never cleaned up, because a quietly rewritten path is a broken image.\n\nAbsolute paths, `..`, backslashes and drive letters are refused.\n\n### Your own cover, card and icon\n\nEvery page gets three pictures: a 16:9 cover for a shared link, a square card\nfor the feed, and a square icon. On the first publish of a new page we make\ncover art from a screenshot of your page and its title. If that fails, we cut\nthe pictures from the screenshot. A new version of a page (with `page_id`) keeps\nthe pictures it has; we make nothing new. To use your own art, send it as\n`cover`, `card` or `icon`:\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"title=Tide Pool\" \\\n  -F \"files=@index.html\" \\\n  -F \"cover=@art/cover.png\" \\\n  -F \"card=@art/card.png\" \\\n  -F \"icon=@art/icon.png\"\n```\n\n| Field | Published size | Smallest accepted |\n|---|---|---|\n| `cover` | 1200×675 | 600×338 |\n| `card` | 1000×1000 | 500×500 |\n| `icon` | 256×256 | 128×128 |\n\n- Send PNG, JPEG or WebP, up to 10 MB each.\n- We scale each picture to fill its size and crop the centre, so send the shape\n  above. We publish it as WebP.\n- Send any of the three, on the first publish or on any later version. A\n  picture you send replaces that picture. A picture you do not send stays as\n  it is.\n- These are not page files. Your page cannot load them by these names.\n- A picture that cannot be used is refused at once with `picture_invalid`, and\n  the message names the field and the reason.\n\n### Publish your dependencies. A CDN will not load.\n\n**This is the mistake to avoid, and it is the one an agent makes by default.**\n\nA published page runs under a Content-Security-Policy that allows `'self'`,\n`data:` and `blob:` — and no external origin. A `<script>` or an `import` from\n`cdn.jsdelivr.net`, unpkg, a Google font host, or anywhere else is **refused by\nthe browser**, and a page whose library never loads draws nothing.\n\nThree libraries are the exception, because mindblown hosts one copy that every\npage shares. Load them by URL and do not upload them:\n\n- `https://mindblown.ai/lib/three-0.185.1/three.min.js` — three.js r185, one\n  file, sets `window.THREE`, with the glTF loader and post-processing passes.\n- `https://mindblown.ai/lib/spark-2.1.0/spark.module.min.js` — Spark 2.1.0, a\n  gaussian-splat renderer, as an ES module.\n- `https://mindblown.ai/lib/phaser-3.90.0/phaser.min.js` — Phaser 3.90.0, one\n  file, sets `window.Phaser`.\n\nFor anything else, include the library's file in the upload and reference it\nby a relative path, such as `./vendor/three.module.js`. A page loads only the\nfiles it was published with.\n\nYou do not have to guess whether you got this right. The render gate loads your\npage **under that exact policy** before anything is published, so a page that\ndepends on a CDN is refused with the blocked origin named, and the version you\nalready have stays live.\n\n### The response: a job, not a page\n\n```json\n{\n  \"job_id\": \"9f1c4a2b6d8e0f31\",\n  \"status_url\": \"/publish/9f1c4a2b6d8e0f31\",\n  \"poll_after_seconds\": 5\n}\n```\n\n`202`. **The publish has started, not finished.** It loads your page in a real\nbrowser twice — once to see what its policy refuses, once to see whether it\ndraws — and that takes longer than anything between you and us is willing to\nhold a connection open for. So the answer comes back at once and you poll, the\nsame way you polled to sign in.\n\nAnything that does not need a browser is still refused **immediately**: a bad\ntoken, an oversized upload, a missing entry file, an unusable path. If you get an\n`error` instead of a `job_id`, nothing was started and polling will not help.\n\n### Poll for the result\n\n```bash\ncurl https://api.mindblown.ai/publish/9f1c4a2b6d8e0f31 \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nWhile it runs:\n\n```json\n{ \"state\": \"running\", \"poll_after_seconds\": 5 }\n```\n\nThat is a `200`. A poll that answered is a poll that succeeded, whatever the\npublish is doing.\n\n**Published:**\n\n```json\n{\n  \"state\": \"done\",\n  \"status\": 200,\n  \"result\": {\n    \"page_id\": \"tide-pool-af261ed3\",\n    \"game_id\": \"d26a1b6d-6116-4d84-a326-72e1af0ffb2c\",\n    \"version\": \"5790ab1f14570d79\",\n    \"url\": \"https://tide-pool.mindblown.ai/\",\n    \"render\": { \"ok\": true }\n  }\n}\n```\n\nThe URL is live when you read it.\n\n**`page_id` is the id to keep.** It is the one this API accepts back, and it is\nwhat makes your next publish a new *version* rather than a new page. `game_id`\nis the registry's id for the same page — it is what a permalink and your library\nare keyed on, and it is **not** interchangeable with `page_id`.\n\n**Refused:**\n\n```json\n{\n  \"state\": \"failed\",\n  \"status\": 422,\n  \"result\": {\n    \"render\": {\n      \"ok\": false,\n      \"reason\": \"the page loads files from origins a published page may not reach; publish those files with the page instead.\",\n      \"missing\": [\"https://cdn.example.com/three.module.js\"]\n    }\n  }\n}\n```\n\nNothing was published. Any previous version is still live and untouched — the\npage is checked **before** a byte goes up, so a refusal never damages what is\nalready there.\n\nThe three terminal answers are `state: \"done\"`, `state: \"failed\"`, and a `404`\non the status URL, which means the job id is unknown or has expired. A job is\nreadable for 30 minutes after it finishes.\n\n## Say when a run starts and ends\n\nIf your page has runs — a level, a round, anything with a beginning and an end —\nsay so. Two lines:\n\n```js\nwindow.mindblown?.event?.(\"game_start\");\nwindow.mindblown?.event?.(\"game_end\");\n```\n\nAnd if it can be paused, the same two words for that:\n\n```js\nwindow.mindblown?.event?.(\"game_pause\");\nwindow.mindblown?.event?.(\"game_resume\");\n```\n\nThe same four words have a shorter form, and it is the same call either way:\n\n```js\nwindow.mindblown?.run?.start();\nwindow.mindblown?.run?.pause();\nwindow.mindblown?.run?.resume();\nwindow.mindblown?.run?.end({ seconds: 41 });\n```\n\n**If your page starts a run before our script has loaded**, say it into a queue\nand it is replayed in order the moment the script arrives. This tag is the last\nthing in the body, so a page that begins at load would otherwise speak into\nnothing:\n\n```js\nwindow.mindblown = window.mindblown || {};\nwindow.mindblown.events = [[\"game_start\"], [\"game_end\", { seconds: 41 }]];\n```\n\n`window.mindblown` is put on your page when it is served. The `?.` is not\ndecoration: it means your code cannot throw on a page opened before that script\nhas loaded, or opened straight from a file while you are building it.\n\n**Why it is worth two lines.** Without them, everything outside your page is\nreading pixels and guessing. \"How long somebody played\" becomes \"how long the\ntab was open\". And a visitor can turn on a recording of their run, which without\nthese words starts at the first input and ends when the picture stops moving —\nso a pause screen ends the clip, because a paused page and a finished one look\nidentical from the outside.\n\nNothing here is required. A page that says none of it still publishes, still\nplays, and is judged no differently.\n\n## Publishing again\n\n**Keep the `page_id` from the result.** Send it back and the page gets a new\nversion at the same URL. It is the `page_id` field and not the `game_id` beside\nit — they are different identifiers and sending the wrong one publishes a\nsecond page.\n\n```bash\ncurl -X POST https://api.mindblown.ai/publish \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"page_id=$PAGE_ID\" \\\n  -F \"files=@index.html\"\n```\n\nA new version keeps the page's pictures. To change one, send it with the new\nversion.\n\nWithout `page_id` you get a NEW page — a new id, a new address, a new row.\nThat is the right thing when it is a new page and the wrong thing when it is\nthe same one, and we cannot tell the difference from the bytes. Two publishes\nof identical files with no `page_id` are two pages, on purpose.\n\nEvery version is kept, and a link shared last week keeps working and serves the\nnewest build.\n\n## Putting an older build back\n\nEvery publish is live the moment it answers, so this is only for going BACK — a\nbuild that broke something, put back without rebuilding it:\n\n```bash\ncurl -X POST https://api.mindblown.ai/me/games/GAME_ID/release \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sha\": \"5790ab1f14570d79\"}'\n```\n\n`sha` is a build of this page — the `version` a publish answered, or a `sha` from\nthe versions list below. Leave it out for the newest, which is how you undo one\nof these. A build this page never published is refused with a 404.\n\n**Ask the person first, every time.** Whoever opens the page gets whichever build\nthis names, and choosing it is theirs — the same rule as making a page public.\n\n## Your pages\n\n```bash\ncurl https://api.mindblown.ai/me/games \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nEach row carries the page's `slug`, `title`, `play_url`, `state`, and its play\ncounts — `play_count`, `unique_players`, `last_played_at`.\n\nThe history of one page:\n\n```bash\ncurl https://api.mindblown.ai/me/games/GAME_ID/versions \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\nWhat visitors hit on a page, how the pages are doing day by day, and recordings\nof how people used them: `references/insights.md`. Read it when\nthe person asks what to fix or how a page is doing.\n\n## Make assets\n\nPictures, cutouts, 3D models (GLB), rigged characters, sound effects, speech\nand video clips, made for the page you build. They are paid from the person's\nmindblown tokens, at cost. The account must have topped up or subscribed at\nleast once; if not, the answer is `402` with `top_up_url`. Send the link to\nthe person.\n\nUse the device-flow token, or an API key the person makes in Settings, on\n<https://mindblown.ai>. A key starts `mb_` and works on these two routes only.\n\n```bash\ncurl -X POST https://api.mindblown.ai/me/assets \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"kind\":\"image\",\"prompt\":\"a small red robot, full body, plain background\"}'\n# 202 {\"id\":\"…\",\"status\":\"queued\",…}\n\ncurl https://api.mindblown.ai/me/assets/ID -H \"Authorization: Bearer $TOKEN\"\n# {\"status\":\"done\",\"url\":\"https://play.mindblown.ai/__mb/gen/<sha>.png\",\"cost_usd\":0.0528,…}\n```\n\nPoll every few seconds. A picture or a sound takes under a minute; a 3D model,\na rig or a clip can take several. `status` is `queued`, `running`, `done` or\n`failed` (with `error`). The `url` is public and permanent: put the file in the\npage, or load it from the page.\n\n| `kind` | Needs | Optional | File |\n|---|---|---|---|\n| `image` | `prompt` | `size` (`square`, `wide`, `tall`), `image_url` to edit a picture | png |\n| `cutout` | `image_url` | | png |\n| `model3d` | `image_url` | `faces` (40,000–1,500,000), `pbr` | glb |\n| `rig` | `model_url` (at most 300,000 faces) | `height_meters` | glb |\n| `sound` | `prompt` | `seconds` (0.5–22), `loop` | mp3 |\n| `speech` | `prompt` (the words) | `voice` | mp3 |\n| `video` | `prompt` | `image_url` to animate, `seconds` (5–15), `resolution` (`480P`, `768P`) | mp4 |\n\nA 3D model is made from a picture: make an `image`, then pass its `url` as\n`image_url`. The subject must fill at least half of that picture, or the model\nfails: ask for one object, full body, on a plain background. An `image_url` or\n`model_url` must be `https`. At most 8 assets of one account are made at a\ntime; the 9th answers `429`.\n\n## What runs inside a published page\n\nEvery page we serve carries one object of ours, `window.mindblown`, injected at\npublish time. You do not add it and you cannot opt out of it.\n\nThrough it a page learns **who is looking** (their handle, and a stable id to\nkeep their things under), says **when a run starts and ends**, reports anything\nelse that happened, and reads the visitor's own **sound switch**.\n\n**The whole of it is in `references/sdk.md`, beside this file.** Read it\nbefore you write a page that has runs, scores, saved progress or a name in it.\n\n### The documents\n\nEach one is beside this file, or is a skill of its own. Read the one that fits\nbefore you write that part of the page.\n\n| Read | When the page |\n| --- | --- |\n| `references/sdk.md` | uses `window.mindblown`: runs, scores, saved progress, the viewer's name |\n| the `mindblown-multiplayer` skill | is used by two or more people together |\n| `references/inventory.md` | takes the person's own car: a slot and our picker |\n| `references/generate.md` | makes text or pictures from what a viewer types |\n\n## Private and public\n\nA published page is live at its own URL immediately, and it starts **private**:\nanyone with the link can open it, and it is not on the person's profile, in\nthe feed or in search. Making it **public** puts it there. That is a second,\nseparate choice, and it is theirs:\n\n```bash\ncurl -X PATCH https://api.mindblown.ai/me/games/GAME_ID \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"listed\": true}'\n```\n\nDo not make a page public without asking the person first.\n\n## What you should tell the person\n\nTwo things, before you publish for them:\n\n1. **A published page opens for anyone with the link.** Private only keeps it\n   off their profile, the feed and search; it does not lock the link. Do not\n   publish anything they would not put on the open web.\n2. **It is on their account.** They signed in; the page is theirs, not yours.\n   They can see it and manage it without you.\n\n## What we will not host\n\nRead this line first, because it is the one that is easy to get backwards: **we\ndo not judge whether the content is good.** Not the style, not the fidelity, not\nthe subject, not whether it looks finished. That is the person's call and it is\nnever ours. A page that renders is a page we host.\n\nWhat we refuse is harm, which is a different axis:\n\n- **Impersonation.** A page presenting itself as a real company, product,\n  person, or publication it is not — their name, their branding, their domain.\n- **Deception that operates.** Sign-in screens, payment forms, or account\n  pages that collect credentials or card details under a false identity.\n  Whether it is \"only a mock\" does not change what it does to whoever opens it.\n- **Fabricated records** shown as genuine: receipts, invoices, transcripts,\n  test results, reviews, screenshots of messages nobody sent.\n- **Malware.** Anything whose purpose is to attack, mine, hijack, or track the\n  person who opened it.\n- **Content targeting a private individual** — their address, their photographs,\n  material made to harass them.\n- **Anything illegal where the page is served.**\n\nWe remove these on report, and repeat offences end the account. Everything we\nremove, we keep the bytes for, so a mistaken removal can be undone.\n\nIf you are unsure whether a page crosses one of these, publish nothing and ask\nthe person. You will usually be asking about impersonation, and they will\nusually know.\n\n## Limits\n\n| | |\n|---|---|\n| Page size | 200 MB per version, all files together |\n| Files | 2,000 per version |\n| Publishes | 60 per hour per account |\n| Reads | 600 per hour per account |\n\nEvery response carries `X-RateLimit-Remaining` and `X-RateLimit-Reset`. A `429`\ncarries `Retry-After` in seconds.\n\n## Errors\n\n```json\n{ \"error\": { \"code\": \"entry_not_in_files\", \"message\": \"entry names index.html, which was not uploaded\" } }\n```\n\n| Code | Status | What to do |\n|---|---|---|\n| `authorization_pending` | 400 | Keep polling. The human has not finished signing in. |\n| `slow_down` | 400 | Double your polling interval. |\n| `expired_token` | 400 | The device code lapsed. Ask for a new one. |\n| `invalid_grant` | 400 | The refresh token is finished. Start the device flow. |\n| `no_token` | 401 | Send `Authorization: Bearer <access_token>`. |\n| `token_rejected` | 401 | The token was refused or names no account. Refresh it, then sign in again. |\n| `not_multipart` | 415 | Send the upload as `multipart/form-data`. |\n| `no_files` | 400 | Attach at least the entry file. |\n| `entry_not_in_files` | 400 | Upload the file named by `entry`. |\n| `bad_path` | 400 | A file name is absolute, contains `..`, or is not portable. |\n| `duplicate_path` | 400 | Two uploaded files have the same path. |\n| `too_many_files` | 400 | Over 2,000 files. |\n| `payload_too_large` | 413 | Over 200 MB. Shrink the assets. |\n| `rate_limited` | 429 | Over the publish ceiling. The message says how long to wait. |\n| `blocked_origin` | 422 | A file loads from a CDN. `render.missing` names the URLs; publish them with the page. |\n| `storage_unavailable` | 503 | Ours. Publishing is off on this deployment; nothing was written. |\n| `token_check_failed` | 502 | Ours, not yours. Wait and retry with the same token. |\n\nA page that uploads cleanly but does not render is **not** in this table. It\nanswers `422` with a `render` object, because nothing about the request was\nwrong — see [If the page does not render](#if-the-page-does-not-render).\n"
}

SHA-256 of public snapshot: 3085efadeb8c420ffa31137311bdccf65925220d5fdd48996d134efff04d810b