Cassette

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 actionStep
Open the link you give them, sign in and approve your code1
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

StepWhatFree gamePaid game or items
1pair with the studio, sign inyesyes
2write cassette.jsonyesyes
3the seller interview (https://cassette.gg/agents/set-up-seller.md)skipyes
4hosting set up (https://cassette.gg/agents/host-game.md)yesyes
5reserve the storefront addressskipyes
6documents, paywall, first deployskipyes
7"Adopt and sign": one sign link creates the storefront and lists the gameskipyes
8deploy (https://cassette.gg/agents/host-game.md)yesyes (again)
9register the game (one relayed transaction)yesyes
10verifyyesyes

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 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.

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).

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:

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.json from 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", 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

ChangeDo
New buildDeploy (https://cassette.gg/agents/host-game.md), then publishBuild with the new build hash (step 9)
Price or itemsupdateListing / 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 policyhttps://cassette.gg/agents/handle-refunds.md
Seller details or documentshttps://cassette.gg/agents/set-up-seller.md (the human adopts again with a set-seller-record sign link), then deploy
Take it off CassettechangeStatus(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

codemeaningfix
bad_nonce / wrong_chain / bad_domainthe sign-in message is offprotocol.md §4: fresh nonce, Chain ID from /v1/config, the API's host as domain
401 unauthorizedmissing or expired bearer tokensign in again (protocol.md §4)
410 on GET /v1/agent-links/<code>the code expired before the human approvedrequest a new code (step 1)
Forbidden in a relay erroryour key isn't an operator of that studio or storefrontstep 1, or a set-operator sign link for the storefront
SlugInUsethe slug belongs to another studiopick another slug
429 rate_limited / relay_budgetthe gas-free limitsprotocol.md §5: wait once, or the human sends it from their wallet
hosting_off / hosting_first_party_onlyCassette doesn't host buildssteps 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.