# publish-game

> Online page: https://cassette.gg/agents/publish-game.md. Fetch it again before you act on it; don't work from a saved copy.
> Start of every task: https://cassette.gg/agents/guide.md.

Goal: the game is listed at `https://cassette.gg/g/<slug>` and playable, served from the developer's own host, and, if it sells,
sold by the developer's own storefront. Publishing is free.

Cassette is a discovery platform for real games that run in the browser. It lists the game and shows it in a frame,
but it doesn't host or sell it. The developer is the seller:

- The build lives in the developer's own hosting account (`https://cassette.gg/agents/host-game.md`).
- A paid game or items are sold by the developer's own **storefront** contract, created from Cassette's admin-less
  factory and signed by the human's own wallet. Your key is only an operator: it lists the game and items.
- The terms of sale, refund policy, privacy notice and imprint are the developer's own, generated from templates after
  you interview the human (`https://cassette.gg/agents/set-up-seller.md`) and adopted by the human's signature.

Cassette holds no role in the storefront, never holds a refund key and can't pause it. Buyers pay the storefront from the
game's own paywall. Cassette's referral fee is 0% for now and only ever applies to sales Cassette referred. You never
handle the human's keys, wallet or money.

**Everything is HTTP and contract calls**: `https://cassette.gg/agents/protocol.md` (config, your key, pairing, sign-in, gas-free
transactions, sign links, hashes). You need `curl`, `jq`, `sha256sum` and a signer (the examples use Foundry's `cast`).

The human acts three times for a game that sells, twice for a free one:

| Human action | Step |
|---|---|
| Open the link you give them, sign in and approve your code | 1 |
| Hosting: nothing if this machine is already logged in to a host; otherwise approve one code (a URL + code, any device) | 4 |
| Read the documents and storefront terms on one review page, then "Adopt and sign" (sells only) | 7 |

## Steps at a glance

| Step | What | Free game | Paid game or items |
|---|---|---|---|
| 1 | pair with the studio, sign in | yes | yes |
| 2 | write `cassette.json` | yes | yes |
| 3 | the seller interview (`https://cassette.gg/agents/set-up-seller.md`) | skip | yes |
| 4 | hosting set up (`https://cassette.gg/agents/host-game.md`) | yes | yes |
| 5 | reserve the storefront address | skip | yes |
| 6 | documents, paywall, first deploy | skip | yes |
| 7 | "Adopt and sign": one sign link creates the storefront and lists the game | skip | yes |
| 8 | deploy (`https://cassette.gg/agents/host-game.md`) | yes | yes (again) |
| 9 | register the game (one relayed transaction) | yes | yes |
| 10 | verify | yes | yes |

## 1. Pair with the developer's studio

You start it (protocol.md §2–§4): make your key, request a code, give the human the link and the code, wait for
`approved`, sign in.

```sh
curl -s -X POST $API/v1/agent-links -H 'content-type: application/json' \
  -d "{\"operator\":\"$ME\",\"agentName\":\"Claude\"}"   # {"code":"K7QX-M2PD","approveUrl":"https://cassette.gg/link/K7QX-M2PD",…}
# tell the human: "Open https://cassette.gg/link/K7QX-M2PD, sign in, and approve the code K7QX-M2PD"
curl -s $API/v1/agent-links/K7QX-M2PD    # {"status":"approved","profileId":"12",…}
```

Keep `PROFILE_ID`, and get the studio owner's wallet (the storefront's owner):
`OWNER=$(curl -s $API/v1/profiles/$PROFILE_ID | jq -r .owner)`.

Tell the human before the first publish: "By publishing you agree to the
Developer Distribution Agreement: https://cassette.gg/developers/terms". Don't publish if they object.

## 2. Describe the game: `cassette.json`

Write `cassette.json` at the root of the build folder (fields: `https://cassette.gg/agents/manifest.md`). Write the tagline and
description yourself from the game. **Ask the human only for what you can't infer**: the title, and if it sells, the
price and where the demo ends.

```json
{"manifestVersion":1,"slug":"blob-run","title":"Blob Run","tagline":"A tiny blob, a vast flooded city",
 "description":"A story-driven platformer about …","genres":["platformer"],"tags":["story","pixel-art"],
 "pricing":{"type":"paid","priceUsd":"4.99"},"demo":{"covers":"Chapter 1","minutes":30},
 "capabilities":["desktop","keyboard","gamepad","audio"],"entry":"index.html",
 "media":{"cover":"cover.png","screenshots":["shot1.png"]},"hosting":{"type":"external","url":"https://placeholder.invalid/"},"items":[]}
```

- `slug`: permanent, 3–48 chars `a-z 0-9 -`; free when `cast call $REG "idOfSlug(string)(uint256)" <slug>` is 0.
- **Pricing is the human's choice**, and many games are free (`"pricing":{"type":"free"}`). For a paid game with depth
  (story-driven, several levels or hours), one option to suggest is a **free demo, then about $5**:
  `"pricing":{"type":"paid","priceUsd":"4.99"}`, `"demo":{"covers":"Chapter 1"}`, and the paywall at the end of
  chapter 1, level N or N minutes in, right after a hook (`https://cassette.gg/agents/add-paywall.md`, "Option: a free demo").
- `capabilities` = what the game really supports. Claim `phone` only with a viewport meta tag and touch input.
- Mature content (realistic violence, drugs, strong language): `"contentRating":"mature"`.
- `hosting.url` is filled in step 9 with the real play URL.

Check it before going on: `jq . cassette.json` parses, every file named in `entry` and `media` exists, the slug matches
`^[a-z0-9][a-z0-9-]{1,46}[a-z0-9]$`.

**Free game without items: skip to step 4.**

## 3. The seller's details (paid game or items)

`https://cassette.gg/agents/set-up-seller.md`: a short interview (who sells, business or private, contact, refund window), kept for step 6.

## 4. Hosting in the developer's account

`https://cassette.gg/agents/host-game.md`: detect a host this machine is already logged into (else one device login for the human), and how
to deploy with that provider's own tool. A game that sells needs a stable origin for its seller record and documents
(any host there except IPFS).

## 5. Reserve the storefront address (paid game or items)

The documents name the storefront, so reserve its address first. One storefront per human sells all their games:
reuse theirs if they have one.

```sh
cast call $FACTORY "storefrontsOf(address)(address[])" $OWNER --rpc-url $RPC      # [] = none yet
SALT=0x$(openssl rand -hex 32)
SF=$(cast call $FACTORY "predict(address,bytes32)(address)" $OWNER $SALT --rpc-url $RPC)
```

Keep `SALT` (step 7 uses it). Add `"storefront":{"chainId":137,"address":"<SF>"}` to `cassette.json`.

## 6. Documents, paywall, first deploy (paid game or items)

1. Documents and the seller record: `https://cassette.gg/agents/set-up-seller.md` writes `legal/*.html` and `.well-known/cassette.json`.
2. The paywall: copy `https://cassette.gg/agents/files/paywall.js` into the build (`https://cassette.gg/agents/add-paywall.md`) and call it where the free part ends
   (`https://cassette.gg/agents/paywall.md`). Nothing else gates a storefront game: without it anyone plays for free.
3. Deploy (`https://cassette.gg/agents/host-game.md`), so the human can read the documents on the developer's site in step 7.

## 7. Adopt and sign: one review page

Prepare one batch for the human (protocol.md §6): the storefront creation and the first listings (at most 8 calls, one
creation).

```sh
TERMS_URL=https://blob-run.pages.dev/legal/terms-of-sale.html; TERMS_SHA=$(sha256sum legal/terms-of-sale.html | cut -c1-64)
RECORD_SHA=$(jq -cSj 'del(.claims, .signature)' .well-known/cassette.json | sha256sum | cut -c1-64)
PARTNER=$(cast call $FACTORY "referralPartner()(address)" --rpc-url $RPC)
CREATE=$(cast calldata "create((address,string,string,(uint32,uint16,address),(address,uint16),string,bytes32,address[]),bytes32)" \
  "(0x0000000000000000000000000000000000000000,\"Pixel Forge\",PIXELFOR,(21600,3,0x0000000000000000000000000000000000000000),($PARTNER,0),https://blob-run.pages.dev/.well-known/cassette.json,0x$RECORD_SHA,[$ME])" $SALT)
LICENCE=$(cast calldata "createListing((uint8,bytes32,bytes32,uint256,uint64,bool,bool,bool,string,string,bytes32))" \
  "(0,$(cast keccak blob-run),$(cast keccak blob-run:licence),4990000,0,false,false,true,\"\",$TERMS_URL,0x$TERMS_SHA)")
```

Inside a `cast` tuple, put names and empty strings in escaped quotes (`\"Pixel Forge\"`, `\"\"`); an unquoted empty
field doesn't parse. Any library (viem `encodeFunctionData`, ethers `Interface`) gives the same bytes.

- `create`: payout `0x0` (= the owner's wallet), the seller's name, a symbol (A–Z 0–9, ≤ 8), the refund policy from the
  interview (seconds, cap, `0x0` or `deployment.abuseRegistry` if they opted in), referral (`referralPartner()`, 0),
  the seller record URL and hash, operators `[your key]`.
- Listings: `https://cassette.gg/agents/sell-items.md` (licence, items, prices in USDC units).
- The link: `calls` = `[{to: FACTORY, data: CREATE}, {to: SF, data: LICENCE}, …]`, `purpose:"create-storefront"`,
  `owner: OWNER`, and `review` with every document (`url`, `sha256`, title) and the summary (protocol.md §6).

Give the human the link: "Read the documents and the storefront terms, then press Adopt and sign". Signing it adopts the
documents (the storefront commits their hashes). Then wait:

```sh
until [ "$(cast call $FACTORY "isStorefront(address)(bool)" $SF --rpc-url $RPC)" = true ]; do sleep 5; done
```

More than 7 listings: the rest go in later batches from your key (it's an operator now), relayed gas-free
(protocol.md §5, storefront-router).

## 8. Deploy again

Deploy the build with the paywall, `legal/` and `.well-known/` (`https://cassette.gg/agents/host-game.md`). A free game: this is its only deploy.
Note the play URL and the build hash it gives you.

## 9. Register the game with Cassette

Set `hosting.url` in `cassette.json` to the play URL, make media paths absolute URLs
on that origin, upload the manifest, then one relayed transaction (protocol.md §5):

```sh
MURI=$(curl -s -X POST $API/v1/metadata -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  --data-binary @cassette.json | jq -r .uri)                                   # sha256:…
GAME_ID=$(cast call $REG "idOfSlug(string)(uint256)" blob-run --rpc-url $RPC | cut -d" " -f1)
# new game:
DATA=$(cast calldata "listGame((uint8,uint8,uint256,bool,uint8,bytes32,uint256,string,string,string),uint256)" \
  "(1,0,0,false,0,$BUILD_HASH,1124,https://blob-run.pages.dev/,$MURI,blob-run)" $PROFILE_ID)
# a new build of a listed game:
DATA=$(cast calldata "publishBuild(uint256,string,uint8,string,bytes32,uint256)" \
  $GAME_ID $MURI 0 https://blob-run.pages.dev/ $BUILD_HASH 1124)
```

Sign and relay `DATA` to `deployment.gameRegistry`; wait for `mined`.

- Tuple order: status (1 published, 2 unlisted, 0 draft), pricing, price, licenceTransferable, hosting (0 external),
  buildHash, capabilities mask, url, metadataURI, slug.
- **A game that sells is registered as free** (pricing 0, price 0): its price and items live in the storefront, and
  the store page reads them from there.
- Cassette stores a pointer (play URL, build hash), never the build. Within minutes its checker fetches
  `/.well-known/cassette-build.json` from the play URL and compares the hash.

## 10. Verify

```sh
curl -s $API/v1/games/blob-run | jq '{status, playUrl, buildHash, pointer, offer, storefront}'
```

- `status: "published"`, `playUrl` answers 200, and `pointer` reports the served build matches.
- A game that sells: `offer.price` is the licence price (from the storefront, within seconds of the indexer seeing it).
  `storefront.problem: "owner-mismatch"` = the storefront isn't owned by the studio owner's wallet.
- Open `https://cassette.gg/play/blob-run`: the game starts; with a demo, play to the cut and the paywall shows.
- A game that sells: the entry page carries the game id, `<meta name="cassette-game" content="$GAME_ID">` (add it
  now, deploy, then `publishBuild` the new hash; `https://cassette.gg/agents/add-paywall.md`). Opened outside Cassette with no wallet, Buy opens
  `https://cassette.gg/connect?game=<GAME_ID>`, never an empty `game=`.

Report to the human: the page URL, the play URL, the price (or free), the storefront address, the refund window, and
that sales reach their wallet once each sale's refund window ends (`https://cassette.gg/agents/handle-refunds.md`).

## Afterwards

| Change | Do |
|---|---|
| New build | Deploy (`https://cassette.gg/agents/host-game.md`), then `publishBuild` with the new build hash (step 9) |
| Price or items | `updateListing` / `createListing` relayed by your key (`https://cassette.gg/agents/sell-items.md`), then upload `cassette.json` and `publishBuild` if the manifest changed |
| Sales, payouts, refunds, refund policy | `https://cassette.gg/agents/handle-refunds.md` |
| Seller details or documents | `https://cassette.gg/agents/set-up-seller.md` (the human adopts again with a `set-seller-record` sign link), then deploy |
| Take it off Cassette | `changeStatus(0, gameId)` (draft) relayed: removes it from discovery only. The storefront and the game keep working on the developer's site |

Limits on gas-free transactions and what to do on a refusal: protocol.md §5 (never retry in a loop).

## Common errors

| code | meaning | fix |
|---|---|---|
| `bad_nonce` / `wrong_chain` / `bad_domain` | the sign-in message is off | protocol.md §4: fresh nonce, `Chain ID` from /v1/config, the API's host as domain |
| 401 `unauthorized` | missing or expired bearer token | sign in again (protocol.md §4) |
| 410 on `GET /v1/agent-links/<code>` | the code expired before the human approved | request a new code (step 1) |
| `Forbidden` in a relay error | your key isn't an operator of that studio or storefront | step 1, or a `set-operator` sign link for the storefront |
| `SlugInUse` | the slug belongs to another studio | pick another slug |
| 429 `rate_limited` / `relay_budget` | the gas-free limits | protocol.md §5: wait once, or the human sends it from their wallet |
| `hosting_off` / `hosting_first_party_only` | Cassette doesn't host builds | steps 4 and 8 |

All codes: `https://cassette.gg/agents/errors.md`.

---

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.
