# Cassette for agents: the protocol

Cassette is an HTTP API plus contracts on Polygon, with nothing to install: you call the API with
`curl` (or any HTTP client), sign with any Ethereum library, and hash with `sha256sum`. The examples use `curl`, `jq`,
`sha256sum` and Foundry's `cast` (`curl -L https://foundry.paradigm.xyz | bash && foundryup`); viem, ethers or
web3.py work the same way.

```sh
API=https://cassette.gg            # the API base; use this exact string everywhere
WEB=https://cassette.gg
CFG=$(curl -s $API/v1/config)
RPC=$(jq -r .rpcUrl <<<"$CFG"); CHAIN=$(jq -r .chainId <<<"$CFG")
```

Errors are `{"error":{"code","message","fix"}}` with an HTTP status (codes: `https://cassette.gg/agents/errors.md`). Reads
need no key. Amounts are decimal strings of USDC units (6 decimals).

## 1. Config and contracts

`GET /v1/config` →
`{chainId, rpcUrl, deployment: {forwarder, profiles, gameRegistry, storefrontFactory, storefrontRouter, refundLedger,
abuseRegistry, storefrontLens, …}, paymentToken: {address, symbol, decimals, permit: {name, version}}, webUrl, api}`.
Read addresses from it every time; never hard-code them.

```sh
REG=$(jq -r .deployment.gameRegistry <<<"$CFG"); PROFILES=$(jq -r .deployment.profiles <<<"$CFG")
FWD=$(jq -r .deployment.forwarder <<<"$CFG"); FACTORY=$(jq -r .deployment.storefrontFactory <<<"$CFG")
ROUTER=$(jq -r .deployment.storefrontRouter <<<"$CFG"); USDC=$(jq -r .paymentToken.address <<<"$CFG")
```

The functions agents use (Solidity signatures, ready for `cast calldata` / `cast call`):

| Contract | Function |
|---|---|
| Profiles | `studioOf(address owner) returns (uint256)` |
| GameRegistry | `idOfSlug(string slug) returns (uint256)` |
| GameRegistry | `listGame((uint8 status, uint8 pricing, uint256 price, bool licenseTransferable, uint8 hosting, bytes32 buildHash, uint256 capabilities, string url, string metadataURI, string slug) input, uint256 profileId)` |
| GameRegistry | `publishBuild(uint256 gameId, string metadataURI, uint8 hosting, string url, bytes32 buildHash, uint256 capabilities)` |
| GameRegistry | `changeStatus(uint8 status, uint256 gameId)` |
| Forwarder | `nonces(address owner) returns (uint256)` |
| StorefrontFactory | `predict(address owner, bytes32 salt) returns (address)`, `isStorefront(address) returns (bool)`, `storefrontsOf(address owner) returns (address[])`, `referralPartner() returns (address)` |
| StorefrontFactory | `create((address payout, string name, string symbol, (uint32 window, uint16 maxSelfRefunds30d, address registry) refundPolicy, (address partner, uint16 bps) referral, string sellerRecordURI, bytes32 sellerRecordHash, address[] operators) params, bytes32 salt)` |
| Storefront | `createListing((uint8 kind, bytes32 game, bytes32 sku, uint256 price, uint64 supply, bool transferable, bool consumable, bool active, string metadataURI, string termsURI, bytes32 termsHash) input)` |
| Storefront | `updateListing(uint256 listingId, uint256 price, bool active, string metadataURI, string termsURI, bytes32 termsHash)` |
| Storefront | `setSellerRecord(string uri, bytes32 hash)`, `setOperator(address operator, bool allowed)`, `setRefundPolicy((uint32,uint16,address))`, `refund(uint256 saleId)`, `release(uint256[] saleIds)` |
| Storefront | `setRefundOverride(address buyer, uint8 value)` (0 default, 1 allow, 2 deny) |
| Storefront (reads) | `listingIdOf(bytes32 sku) returns (uint256)`, `listingsOfGame(bytes32 game) returns (uint256[])`, `listing(uint256 listingId) returns ((uint8 kind, bytes32 game, bytes32 sku, uint256 price, uint64 supply, uint64 sold, bool transferable, bool consumable, bool active, string metadataURI, string termsURI, bytes32 termsHash))`, `owner() returns (address)`, `sellerRecord() returns (string uri, bytes32 hash)`, `quote(uint256 listingId, uint64 qty, address referrer) returns (uint256 total, uint256 fee)`, `sale(uint256 saleId)`, `releasable(uint256 saleId) returns (bool)`, `hasLicense(address account, bytes32 game) returns (bool)` |
| Storefront (players) | `buy(uint256 listingId, uint64 qty, address recipient, uint256 maxTotal, address referrer, bytes32 ref, bytes32 expect)`, `buyWithPermit(…, bytes32 expect, uint256 deadline, bytes signature)`, `selfRefund(uint256 saleId)`, `termsCommitment(uint256 listingId, address payer) returns (bytes32)`, `balanceOf(address account, uint256 id) returns (uint256)` |
| StorefrontRouter | `routeBatch((address target, bytes data)[] calls)`, `nonces(address) returns (uint256)` |

Enums: `status` draft 0, published 1, unlisted 2; `pricing` free 0, free-with-items 1, paid 2; `hosting` external 0,
ipfs 2; listing `kind` license 0, item 1. `capabilities` is a bit mask: phone 0, tablet 1, desktop 2, touch 3, mouse 4,
keyboard 5, gamepad 6, multiplayer-local 7, multiplayer-online 8, offline 9, audio 10 (`phone,desktop,touch` = 1+4+8
= 13). The full storefront interface, events and errors: `https://cassette.gg/agents/paywall.md`.

`cast call` prints numbers of 10000 and above as `10000 [1e4]`: keep the first word (`| cut -d" " -f1`) before
reusing one. Example: is a slug free?

```sh
cast call $REG "idOfSlug(string)(uint256)" blob-run --rpc-url $RPC   # 0 = free
```

## 2. Your key

Make one EOA for this developer and keep it outside the game folder. It signs your API logins and your gas-free
transactions; it never holds the human's money and never owns their studio or storefront (it's an operator).

```sh
mkdir -p ~/.cassette && cast wallet new --json | jq '.data[0] // .[0]' > ~/.cassette/agent.json && chmod 600 ~/.cassette/agent.json
KEY=$(jq -r .private_key ~/.cassette/agent.json); ME=$(cast wallet address --private-key $KEY)
```

Never print the key, put it in a URL, or send it anywhere (Cassette included).

## 3. Pairing: the human links you to their studio

You start it; the human needs no id from the website. Request a code for your key:

```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","expiresAt":1793212345,"expiresIn":600}
```

Tell the human: "Open <approveUrl>, sign in, and approve the code K7QX-M2PD (or type the code at
https://cassette.gg/publish)." The code is on the page so they can check it's yours. Then wait:

```sh
until s=$(curl -s $API/v1/agent-links/$CODE | jq -r .status); [ "$s" != pending ]; do sleep 3; done
curl -s $API/v1/agent-links/$CODE   # {"status":"approved","profileId":"12","operator":"0x…",…}
```

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

Approved: your key is now an operator of studio `profileId` (a first-time human creates the studio on that page).
A code lasts 10 minutes and works once. Expired (410, or still `pending` after `expiresAt`): request a new one and
show it again. One key gets at most 10 codes an hour.

## 4. Sign in (SIWE, EIP-4361)

```sh
NONCE=$(curl -s "$API/v1/auth/nonce?address=$ME" | jq -r .nonce)
HOST=$(sed -E 's#^https?://([^/]+).*#\1#' <<<"$API"); ORIGIN=$(sed -E 's#^(https?://[^/]+).*#\1#' <<<"$API")
MSG="$HOST wants you to sign in with your Ethereum account:
$ME

Sign in to Cassette

URI: $ORIGIN
Version: 1
Chain ID: $CHAIN
Nonce: $NONCE
Issued At: $(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
SIG=$(cast wallet sign --private-key $KEY "$MSG")
TOKEN=$(jq -n --arg m "$MSG" --arg s "$SIG" '{message:$m,signature:$s}' \
  | curl -s -X POST $API/v1/auth/verify -H 'content-type: application/json' -d @- | jq -r .token)
```

Send `Authorization: Bearer $TOKEN` on writes (valid 7 days). `$ME` must be the checksummed address (what
`cast wallet address` prints); the domain is the API's host.

## 5. Gas-free transactions (the relay)

Your key never needs POL. You sign an ERC-2771 `ForwardRequest` and Cassette's relayer sends it.

| Target | EIP-712 domain | nonce from | `forwarder` in the body |
|---|---|---|---|
| Profiles, GameRegistry | `{name:"CassetteForwarder", version:"1", chainId, verifyingContract: deployment.forwarder}` | `forwarder.nonces(you)` | omit |
| a storefront or the factory (operator calls) | `{name:"CassetteStorefrontRouter", version:"1", chainId, verifyingContract: deployment.storefrontRouter}` | `router.nonces(you)` | `"storefront-router"` |

Worked example: register a new game (`listGame`) through the platform forwarder.

```sh
DATA=$(cast calldata "listGame((uint8,uint8,uint256,bool,uint8,bytes32,uint256,string,string,string),uint256)" \
  "(1,0,0,false,0,$BUILD_HASH,13,$PLAY_URL,$MANIFEST_URI,blob-run)" $PROFILE_ID)
NONCE=$(cast call $FWD "nonces(address)(uint256)" $ME --rpc-url $RPC | cut -d" " -f1)
DEADLINE=$(( $(date +%s) + 900 ))
jq -n --arg from $ME --arg to $REG --arg data $DATA --arg nonce $NONCE --argjson deadline $DEADLINE \
  --argjson chain $CHAIN --arg fwd $FWD '{
  types: {
    EIP712Domain: [{name:"name",type:"string"},{name:"version",type:"string"},{name:"chainId",type:"uint256"},{name:"verifyingContract",type:"address"}],
    ForwardRequest: [{name:"from",type:"address"},{name:"to",type:"address"},{name:"value",type:"uint256"},{name:"gas",type:"uint256"},{name:"nonce",type:"uint256"},{name:"deadline",type:"uint48"},{name:"data",type:"bytes"}]
  },
  primaryType: "ForwardRequest",
  domain: {name:"CassetteForwarder", version:"1", chainId:$chain, verifyingContract:$fwd},
  message: {from:$from, to:$to, value:"0", gas:"1000000", nonce:$nonce, deadline:$deadline, data:$data}
}' > /tmp/req.json
SIG=$(cast wallet sign --data --from-file /tmp/req.json --private-key $KEY)
TX=$(jq '{request: ((.message | {from,to,value,gas,deadline,data}) + {signature: $sig})}' --arg sig $SIG /tmp/req.json \
  | curl -s -X POST $API/v1/relay -H 'content-type: application/json' -d @- | jq -r .txHash)
until s=$(curl -s $API/v1/relay/$TX | jq -r .status); [ "$s" != pending ]; do sleep 2; done; echo $s   # mined
```

- `gas`: 1000000 suits every call here; at most 1500000. `value` is always `"0"`. `deadline` within the hour.
- Storefront calls: the same with the router domain, `nonce` from `cast call $ROUTER "nonces(address)(uint256)" $ME | cut -d" " -f1`,
  `to` = the storefront, and `"forwarder":"storefront-router"` next to `request`.
- A call that would revert is refused with the decoded reason in `error.message` (e.g. `SlugInUse`, `Forbidden`: this
  key isn't an operator of that studio).

**Limits:** per key 20 relays per minute and 500 per day; at most 8 calls in one routed batch, one storefront creation
per batch (`GET /v1/wallet/rules` has the current numbers). On a 429 read `error.message`: "retry in Ns" means wait
that long once; "for today" or `relay_budget` means stop and tell the human (they can send the transaction from their
own wallet and pay the network fee). Never retry in a loop. Before bulk work (dozens of listings), read the rules and
tell the human how long it will take.

## 6. Sign links: what only the human may sign

Creating the storefront, changing its refund policy, refunding a sale and adding an operator need the storefront
owner's wallet. Prepare the calls and give the human a link; the page shows each call in plain words and sends them
as one `router.routeBatch` from their wallet. Cassette stores nothing: the link carries it all.

```json
{"v":1,"chainId":137,"purpose":"create-storefront","agent":"0xYou","owner":"0xHuman",
 "calls":[{"to":"0xFactory","data":"0x…"},{"to":"0xStorefront","data":"0x…"}],
 "summary":["Create your storefront …","List Blob Run for 4.99 USDC"],
 "review":{"documents":[{"kind":"terms","title":"Terms of sale","url":"https://…/legal/terms-of-sale.html","sha256":"…"}],
   "summary":{"game":"blob-run","title":"Blob Run","priceUsd":"4.99","items":0,"refundWindowSec":21600,
     "maxSelfRefunds30d":3,"abuseRegistry":false,"referralBps":0,"operator":"0xYou"}}}
```

`purpose`: `create-storefront`, `sync-listings`, `set-refund-policy`, `set-refund-override`, `set-seller-record`,
`set-operator` or `refund`. `calls` go to the factory or one of its storefronts (≤ 8). `approvals`
(`[{token, spender, amount}]`) only for an owner refund after release. `review` makes it the "Adopt and sign" page:
the page fetches every document and checks its sha256, and signing adopts them.

```sh
URL="$WEB/studio/sign#$(jq -cj . req.json | base64 -w0 | tr '+/' '-_' | tr -d '=')"
```

Send the link and wait for the effect on chain, e.g. `cast call $FACTORY "isStorefront(address)(bool)" $SF`.

## 7. Hashes

| What | How |
|---|---|
| A document or file | `sha256sum legal/terms-of-sale.html` (64 hex; on chain as `0x<hex>`) |
| The build (`buildHash`) | the recipe in `https://cassette.gg/agents/host-game.md` (`.well-known/cassette-build.json`) |
| The seller record | `jq -cSj 'del(.claims, .signature)' .well-known/cassette.json \| sha256sum` (keys sorted, no whitespace) |
| Game key | `cast keccak blob-run` |
| Licence sku | `cast keccak blob-run:licence` |
| Item sku | `cast format-bytes32-string neon-trail` |
| Purchase commitment (`expect`) | `cast keccak $(cast abi-encode "f(bytes32,bytes32,uint32,uint16,address,uint8)" $TERMS_HASH $RECORD_HASH $WINDOW $CAP $REGISTRY $OVERRIDE)`, equal to `termsCommitment(listingId, payer)` |

## 8. Formats

- **`cassette.json`** (the game manifest, in the build folder): `https://cassette.gg/agents/manifest.md`. Upload it with
  `POST /v1/metadata` (bearer, the JSON as the body) → `{"uri":"sha256:…","url":"…"}`; `uri` is the `metadataURI`.
- **Seller record** (`/.well-known/cassette.json` on the game's host, or its own `ar://` item):

  ```json
  {"version":1,
   "seller":{"name":"Pixel Forge","business":true,"country":"ES","contact":"help@pixelforge.dev",
             "legalName":"Pixel Forge S.L.","address":"…","email":"help@pixelforge.dev","vatNumber":"ESB12345678"},
   "storefronts":[{"chainId":137,"address":"0xStorefront"}],
   "legal":{"terms":{"url":"https://…/legal/terms-of-sale.html","sha256":"…"},
            "refunds":{"url":"…","sha256":"…"},"privacy":{"url":"…","sha256":"…"}},
   "games":["blob-run"],"issuedAt":"2026-10-30T16:00:00.000Z"}
  ```

  Only these fields. The storefront commits its hash (`sellerRecordHash`); `claims` (community claims) and
  `signature` sit outside the hash. A business adds `imprint` and `withdrawalForm` to `legal`.
- **Build manifest** `/.well-known/cassette-build.json`: `https://cassette.gg/agents/host-game.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.
