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.
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.
{"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 charsa-z 0-9 -; free whencast 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. Claimphoneonly with a viewport meta tag and touch input.- Mature content (realistic violence, drugs, strong language):
"contentRating":"mature". hosting.urlis 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.
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)
- Documents and the seller record:
https://cassette.gg/agents/set-up-seller.mdwriteslegal/*.htmland.well-known/cassette.json. - The paywall: copy
https://cassette.gg/agents/files/paywall.jsinto 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. - 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).
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: payout0x0(= the owner's wallet), the seller's name, a symbol (A–Z 0–9, ≤ 8), the refund policy from the interview (seconds, cap,0x0ordeployment.abuseRegistryif 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, andreviewwith 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:
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):
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.jsonfrom the play URL and compares the hash.
10. Verify
curl -s $API/v1/games/blob-run | jq '{status, playUrl, buildHash, pointer, offer, storefront}'
status: "published",playUrlanswers 200, andpointerreports the served build matches.- A game that sells:
offer.priceis 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, thenpublishBuildthe new hash;https://cassette.gg/agents/add-paywall.md). Opened outside Cassette with no wallet, Buy openshttps://cassette.gg/connect?game=<GAME_ID>, never an emptygame=.
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.