# Error codes and fixes

HTTP API errors: non-2xx with `{"error":{"code","message","fix?"}}`. Contract calls revert with a named error; the relay
(`POST /v1/relay`) refuses a call that would revert with `relay_reverted` and the name in `message`, and the paywall and
the sign page decode them the same way. Apply the fix and retry once; never retry in a loop.

## HTTP API (`code` uses underscores)
| code | status | meaning / fix |
|---|---|---|
| `bad_request`, `bad_json`, `bad_address`, `bad_amount`, `bad_message` | 400 | malformed input; fix the body/query |
| `bad_domain`, `bad_nonce`, `bad_signature`, `expired`, `wrong_chain` | 400/401 | SIWE login problems: request a fresh nonce and sign the new message |
| `unauthorized`, `bad_token` | 401 | missing/expired bearer token: log in again |
| `forbidden`, `no_access`, `no_license`, `not_operator`, `no_profile` | 403/409 | caller lacks rights: use the studio owner/operator key |
| `read_the_storefront` | 409 | licence attestation asked for a storefront game: read the developer's storefront on chain instead (`https://cassette.gg/agents/verify-ownership.md`) |
| `refunded` | 403 | legacy Checkout games only (Cassette's own refund record). Storefront refunds burn the tokens instead |
| `hosting_off` / `hosting_first_party_only` | 410/403 | Cassette doesn't host builds: host in your own account (`https://cassette.gg/agents/host-game.md`), then register the pointer (`https://cassette.gg/agents/publish-game.md`) |
| `personal_data_in_reason` | 422 | a public takedown reason contains an email, phone number, @username or wallet address: describe the broken rule instead |
| `target_not_allowed`, `function_not_allowed`, `value_not_allowed`, `gas_too_high`, `deadline_too_far`, `bad_request_signature` | 400/403 | relay policy: only platform contracts or factory storefronts, no token transfers or approvals, value 0, gas ≤ 1.5 M, deadline ≤ 24 h |
| `relay_reverted` | 422 | the call would revert; message names the reason |
| `relay_unavailable` | 503 | relayer, or the storefront router, not deployed: retry later |
| `not_found`, `not_ready`, `not_playable`, `delisted` | 404 | unknown / still indexing (retry in seconds) / not playable |
| `already_claimed`, `already_released` | 409 | community listing state |
| `invalid_manifest`, `missing_manifest`, `bad_zip`, `unsafe_path`, `too_many_files`, `too_large`, `data_too_large` | 413/422 | legacy build upload problems |
| `rate_limited` | 429 | wait and retry (relay: 20 tx/min and 500/day per wallet) |
| `faucet_unavailable`, `not_deployed` | 503 | platform not fully up: retry later |

## Contract reverts (storefront, factory, router)

Decode a revert's first 4 bytes with `cast 4byte-decode <data>` or your library's ABI decoder
(`https://cassette.gg/agents/paywall.md` lists the storefront's errors).

| error | meaning | fix |
|---|---|---|
| `NotStorefront` | That address is not a storefront created by the Cassette storefront factory. | Check the storefront address in cassette.json (factory.isStorefront must be true). |
| `ERC2771UntrustfulTarget` | That address is not a storefront created by the Cassette storefront factory. | Check the storefront address in cassette.json (factory.isStorefront must be true). |
| `NotOwner` | Only the storefront's owner can do that. | The human (the owner wallet) signs it: send them a sign link (protocol.md §6). |
| `NotOperator` | This key can't manage the storefront's listings. | Send the human a `set-operator` sign link for this key, or put the call itself in a sign link (protocol.md §6). |
| `ListingInactive` | It is not on sale right now. | Turn it back on with `updateListing(…, active true, …)` (`https://cassette.gg/agents/sell-items.md`). |
| `NoSuchListing` | That item or licence is not listed. | Create the listing (`createListing`, `https://cassette.gg/agents/sell-items.md`). |
| `AlreadyLicensed` | This wallet already owns the game. | Nothing to buy: refresh ownership. |
| `FreeClaimUsed` | This wallet already claimed it. | Nothing to claim: refresh ownership. |
| `PriceOverMax` | The price went up since it was shown. Nothing was charged. | Show the new price and ask again. |
| `PermitAmountMismatch` | The payment authorisation didn't match the price. Nothing was charged. | Sign a permit for exactly the total (quote). |
| `BadQuantity` | That quantity is not allowed. | Licences and free listings are 1 per wallet; quantities must be at least 1. |
| `SelfRefundUnavailable` | This purchase can't be refunded from here. | Check canSelfRefund(saleId) for the reason (window closed, gift, cap, seller decision, registry); the seller can always refund. |
| `NotReleasable` | That sale is still inside its refund window, or already paid out. | Release after the window ends (`release([saleId])`, `https://cassette.gg/agents/handle-refunds.md`). |
| `AlreadyRefunded` | That sale was already refunded. | Nothing to do. |
| `BadRefundPolicy` | Refund window over 14 days, or a registry other than Cassette’s. | Use a window of 0–336 hours and the factory abuse registry or none. |
| `BadReferral` | Referral fee over 30%, or a fee without a partner. | Keep the referral as the factory proposes it. |
| `BadText` | A name or URL is too long or has characters a storefront rejects. | URLs: visible ASCII, at most 512 bytes. |
| `BadListing` | The listing is out of bounds (price, supply, or a consumable licence). | Prices < 2^128, supply ≤ 2^53−1, only items may be consumable. |
| `TermsChanged` | The seller changed the terms, the seller details or the refund rules since they were shown. Nothing was charged. | Show the current terms again (refresh) and ask again. |
| `NoSellerRecord` | The storefront needs its seller record (URI and hash) first. | Publish the seller record: `setSellerRecord(uri, hash)` (`https://cassette.gg/agents/set-up-seller.md`). |
| `ERC2771ForwarderExpiredRequest` | The signed request expired before it was sent. | Re-run the command (it signs a fresh request). |
| `ERC2771ForwarderInvalidSigner` | The signed request doesn't match its sender. | Re-run the command with the same key. |
| `InvalidAccountNonce` | The signed request was already used or out of order. | Re-run the command (it reads the current nonce). |
| `SupplyExhausted` | Sold out. | List a new item (supply is fixed per listing). |
| `TokensGone` | What was bought was transferred or used, so it cannot be returned. | The seller can still refund it. |
| `SkuInUse` | That sku is already listed in this storefront. | Update the existing listing (`updateListing`) or pick another sku. |

## Paywall and play-frame results

`StorefrontTxResult.reason` and `TopUpResult.reason` from the play-frame bridge (`https://cassette.gg/agents/paywall.md`):
`cancelled` (the player closed the wallet; nothing charged), `signed-out`, `wrong-chain`, `not-a-storefront` (the
address isn't a factory storefront: payment stopped), `unsupported` (this host can't do it: gifts, or a top-up where
none is offered), `insufficient-funds` (say how much USDC to add; the host's top-up when it offers one), `failed` (with `error.name` when the chain refused).

## Hosting

Hosting tool failures (login, limits, names taken), Arweave funding (`host-fund`, once Arweave opens) and games a
host can't serve (`arweave-unsuitable`): `https://cassette.gg/agents/host-game.md`.
