# cassette.json (manifest)

Lives at the root of the game folder. Checked by the API against the schema below when you upload it (`POST /v1/metadata`) (`manifestVersion: 1`). Unknown fields are ignored.

| Field | Type | Required | Default / rule |
|---|---|---|---|
| `manifestVersion` | `1` | yes | literal `1` |
| `slug` | string | yes | `^[a-z0-9][a-z0-9-]{1,46}[a-z0-9]$` (3-48 chars). Unique, **immutable** after first publish |
| `title` | string | yes | 1-60 chars |
| `tagline` | string | no | ≤ 80 chars; keep ≤ 6 words (shown on covers) |
| `description` | string | no | ≤ 2000 chars |
| `genres` | string[] | no | ≤ 3 items, each ≤ 24 chars. Default `[]` |
| `tags` | string[] | no | ≤ 10 items, each ≤ 24 chars. Default `[]` |
| `capabilities` | string[] | yes | ≥ 1 of `phone tablet desktop touch mouse keyboard gamepad multiplayer-local multiplayer-online offline audio`; at least one of phone/tablet/desktop |
| `orientation` | `portrait`\|`landscape`\|`any` | no | `any` |
| `pricing` | object | yes | `{"type":"free"}` · `{"type":"free-with-items"}` · `{"type":"paid","priceUsd":"4.99","licenseTransferable":false}` |
| `demo` | boolean or object | no | A free demo, then the licence to keep playing: `true`, or `{"minutes": 30, "covers": "Chapter 1"}` (≤ 600 min, ≤ 120 chars); `false` = pay to play. Unset: a demo when the storefront sells a licence. The game's own paywall decides where it ends; the store shows "Free demo · $4.99" |
| `items` | object[] | no | ≤ 100, requires pricing ≠ `free`. See below. Default `[]` |
| `entry` | string | no | `index.html` (path of the HTML file inside the folder) |
| `media` | object | yes | `cover` (required, 16:9 image path or https URL), `icon` (square, for the installable app), `screenshots` (≤ 12), `trailer` (URL) |
| `contentRating` | `everyone`\|`teen`\|`mature` | no | `everyone`. Realistic violence or gore, drug use or strong language needs `"mature"` (content policy); the store page then shows a Mature marker. |
| `license` | string | no | ≤ 64 chars, SPDX id e.g. `MIT` |
| `sourceUrl` | https URL | no | link to the source repo |
| `hosting` | object | no | `{"type":"hosted"}` (default; legacy: Cassette hosts builds for its own games only) · `{"type":"external","url":"https://..."}` · `{"type":"ipfs","cid":"..."}`. A developer's game is hosted in their own account (`https://cassette.gg/agents/host-game.md`) and registered as `{"type":"external","url":"<play URL>"}` (`ar://<id>` on Arweave) |
| `communityListingId` | string | no | set when converting a claimed community listing |
| `storefront` | object | no | The developer's own storefront contract (set it in publish-game step 5) that sells this game: `{"chainId":137,"address":"0x…","game":"0x…"?,"license":"1"?,"items":{"<sku>":"2"}}`. `game` defaults to keccak256(slug); `license`/`items` are listing ids. The paywall reads it from the served cassette.json |
| `previousStorefronts` | object[] | no | Earlier storefronts of the same developer (same shape as `storefront`, ≤ 8): their licences and items still count as owned |
| `legal` | object | no | The adopted documents (`https://cassette.gg/agents/set-up-seller.md`): `{"terms":{"url","sha256"},"refunds"?,"privacy"?,"imprint"?,"withdrawalForm"?}`, each on the developer's own origin with the sha256 of the served bytes |
| `mirrors` | https URL[] | no | ≤ 4 mirrors of the build in the developer's own accounts (e.g. an IPFS subdomain gateway). Add them when you register the game |

`priceUsd`: `^\d{1,7}(\.\d{1,2})?$` (> 0 for paid).

## items[]

| Field | Type | Rule |
|---|---|---|
| `sku` | string | `^[a-z0-9][a-z0-9._-]{0,30}$`, unique per game, **permanent** |
| `name` | string | 1-48 chars |
| `description` | string | ≤ 280 chars |
| `priceUsd` | string | USD, ≤ 2 decimals; `"0"` = free claim |
| `maxSupply` | integer | `0` = unlimited. **Immutable** after first sync |
| `transferable` | boolean | default `true`. **Immutable** |
| `consumable` | boolean | default `false`. **Immutable** |
| `image` | string | path inside the build or https URL |

Media paths for `hosted` games are relative to the folder and must exist; for `external`/`ipfs` hosting they must be absolute https URLs. Keep relative paths in the build's copy; in the copy you upload (`POST /v1/metadata`), make them absolute URLs on the play origin.

## Minimal example

```json
{
  "manifestVersion": 1,
  "slug": "blob-run",
  "title": "Blob Run",
  "capabilities": ["phone", "desktop", "touch", "keyboard"],
  "pricing": { "type": "free" },
  "media": { "cover": "cover.svg", "icon": "icon.svg" }
}
```

---

Code in this document (and `paywall.js`) is MIT-licensed: `SPDX-License-Identifier: MIT`, Copyright (c) 2026 Delegas
Labs, S.L. Copy and adapt it freely.
