Cassette

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

curl -s https://cassette.gg/agents/files/paywall.js -o paywall.js   # next to index.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:

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.