# add-paywall

> Online page: https://cassette.gg/agents/add-paywall.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.

A storefront game is gated only by its own paywall: nothing else stops anyone who opens the play URL. You copy
one reference file and adapt it. The protocol it follows (contract reads, the purchase
commitment, the play-frame bridge, the order confirmation): `https://cassette.gg/agents/paywall.md`.

**Before this page:** the game has a storefront with a licence listing (`https://cassette.gg/agents/publish-game.md`, steps 3–7) and
`cassette.json` names it (`storefront.address`).

## 1. Add the file

```sh
curl -s https://cassette.gg/agents/files/paywall.js -o paywall.js   # next to index.html
```

```html
<script src="paywall.js"></script>
```

**The game id is required.** Once the game is registered (`https://cassette.gg/agents/publish-game.md`, step 9) put it in the entry page:
`<meta name="cassette-game" content="<GAME_ID>">` (`curl -s https://cassette.gg/v1/games/<slug> | jq -r .id`). Until then
`paywall.js` looks it up from the slug in `cassette.json`, the storefront or the page URL. Without it, players who have
no wallet can't sign in to Cassette to pay, and the paywall shows a setup error.

It reads `cassette.json` next to the page, the seller record and the listing from the chain (public Polygon RPCs; pass
`rpcUrl` for the developer's own keyed RPC from any provider's free tier, restricted to the game's origin; Cassette
supplies none). Adapt texts and look freely. Keep the purchase rules: the consent box, the price and terms shown, the
`expect` commitment from what was shown, and the order confirmation before play (EU law needs it on a durable medium).

## 2. Option: a free demo, then the full game

Pricing is the developer's call, and many games on Cassette are free. For a paid game with depth (story-driven
single-player, WASM, three.js, WebGPU), one option worth suggesting is a **free demo**: the start plays free, and
**about $5** (`"priceUsd": "4.99"`) unlocks the rest and the ending. Gating from the first second is just as valid;
ask the human which they want. Set `"demo": {"covers": "Chapter
1", "minutes": 30}` in `cassette.json` so the store tile says what's free (`"demo": false` for a game gated from the
start).

Call it where the demo ends:

```js
async function onChapterComplete(n) {
  if (n === 1 && !(await CassettePaywall.unlock({ demo: true, dismissible: true }))) return showTitleScreen()
  startChapter(n + 1) // owners pass at once; buyers continue right here
}
```

`unlock()` resolves `true` at once for a player who owns the game; otherwise the paywall opens with "That's the end of
the <title> demo" and the price, and resolves `true` after the purchase (and the order confirmation) or `false` on "Not
now". Call it again to reopen it.

**Where to cut**: at a boundary the game already has (a chapter end, a level-complete screen, the return to a hub),
never mid-level, mid-dialogue or mid-fight.
- Story or level-based game: the end of chapter (or act) 1, or after level N where N is about 15-25% of the game. Aim
  for 20-40 minutes for a first-time player.
- Open or systemic game (sandbox, roguelike, sim, survival): after about 30-45 minutes of play, N runs or days, or the
  moment the second area opens. Count play time yourself and keep it in the saves so a reload doesn't reset it.
- Never before the game's best idea: the demo must show the core loop and the first real "aha" (the mechanic, twist or
  set piece that makes this game itself).

**End on a hook**: stop right after a cliffhanger, a reveal, a new ability or weapon, a boss door, or a glimpse of a new
area, not after a lull. Put what's next in the paywall line: `CassettePaywall.unlock({ demo: 'Chapter 2: the flooded city, and
the truth about Mara.', dismissible: true })`. After buying, the game must continue from exactly
that point with the same save, never a restart. Keep progress where a new build can still read it (the shell's saves, below), and say
"Free demo" in the tagline or description.

**Price**: suggest $4.99 for a few hours of play. Use $1.99-2.99 for a game under about 2 hours, and up to $9.99 for a
long, polished one. The human decides. A player without enough USDC is told how much to add and where. When Cassette's card top-up ships, the host offers
**Top up** through the `top-up` feature and nothing in your game changes. Don't describe cards to the player today.

Gated from the start instead: `CassettePaywall.unlock().then(startGame)` (no "Not now").

## 3. What players see

- The seller's name (business or private), the price in USDC, the seller's terms and refund policy, and the
  immediate-delivery consent box for business sellers ("Deliver it now. I understand I lose my 14-day right to cancel
  once it's delivered.").
- The refund line: self-refund within the seller's window, or why not (a gift, a limited edition, the seller's cap).
- Inside Cassette the Cassette wallet sheet pays. Elsewhere "Buy" opens Cassette sign-in (`/connect`) by default, even
  when the browser has a wallet: Cassette's sheet pays, with no network fee for Cassette accounts. "Pay with my own
  wallet" is the alternative (it pays its own network fee). A player without enough USDC
  is told how much to add and where. When Cassette's card top-up ships, the host offers **Top up** through the
  `top-up` feature and nothing in your game changes. Don't describe cards to the player today.
- After paying: "Save confirmation and play" (a one-file HTML order confirmation with the seller's details and the
  hash-checked terms), then the game unlocks.
- Store page "Buy full game": the shell opens the game with `intent: "buy"` and the paywall opens at once.

## 4. Saves and the shell

Inside Cassette, `await CassettePaywall.host()` is the shell: `request({type:'saves', op:'set', key:'slot1', value})`
keeps saves per game and wallet, so they survive a new build at a new origin (Arweave gives every build its own). Keep
no secrets in `localStorage` or saves. Other messages (`ready`, `track`): `https://cassette.gg/agents/paywall.md`, §5 "Bridge messages".

## 5. Check it works

1. The game id resolves: `curl -s https://cassette.gg/v1/games/<slug> | jq -r .id` prints `GAME_ID`, and the served page carries
   it: `curl -s <play URL> | grep -o '<meta name="cassette-game" content="[0-9]*">'` (or the served `cassette.json`
   has that slug). The browser console shows no `[paywall]` error.
2. A private window with no wallet on the play URL: play to the cut; the paywall shows "Sold by <name> (<legal name>)"
   for a business seller, Buy opens Cassette sign-in (`/connect?game=<GAME_ID>`), and "Not now" returns to the menu.
3. `https://cassette.gg/play/<slug>` signed in: buy, save the confirmation, the game continues from the cut.
4. Reload as the buyer: `unlock()` passes at once.

---

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.
