# GrowBlocks Dev Kit

Build your own game on GrowBlocks. You write the game in JavaScript: its mechanics, its physics, its rules. GrowBlocks gives you:

- the engine: three.js rendering, Rapier physics, and the GrowBlocks characters
- multiplayer rooms on our match server
- the players' accounts, wallets and market
- hosting, a page in the game, and players

When your game's collections sell and its coin trades, GrowBlocks takes its usual fee, and **70% of that fee is yours**.

Portal: `devkit.html`, where you create a game, upload builds, see your status and claim earnings.

## Quick start

You need Node.js 18 or later.

```
npx growblocks new my-game     # the starter kit, always the latest
cd my-game
npx growblocks dev             # play it at http://localhost:5173/play.html
```

play.html loads the GrowBlocks engine from growblocks.xyz and runs your `game.js`, with multiplayer in local test rooms. Edit `game.js` and reload. You can also open your build inside GrowBlocks itself: `https://growblocks.xyz/game.html?dev=http://localhost:5173/game.js` (Chrome asks whether growblocks.xyz may access your local network; allow it).

**With Claude Code** (recommended): run `claude` in the folder and describe your game, for example: *"Make a multiplayer rooftop race on GrowBlocks. Check it with a screenshot and fix anything that's off, then push a test build and give me the link."* The folder's `.mcp.json` gives Claude the GrowBlocks tools (below), so it reads the API, checks its own work, adds blocks and ships the test build by itself.

Local builds simulate the chain: `gb.chain.mint()` pretends to mint and `gb.chain.balance()` returns test numbers. You can build your whole flow before review. No Node.js? Download the same starter kit as a zip from `devkit.html` and serve the folder with any web server that allows CORS (`npx serve --cors -l 5173` if you have npx).


The kit is made to be written by an AI assistant. **Download the starter kit** from `devkit.html` (Build it with AI). It contains:

| | |
|---|---|
| `game.js` | the starter game |
| `AGENTS.md`, `CLAUDE.md` | instructions coding agents read by themselves (Claude Code, Cursor, Copilot, Codex) |
| `llms.txt` | the complete API, rules, patterns and pitfalls, written for a model (also at `/devkit/llms.txt`) |
| `gb.d.ts` | the API as TypeScript types |
| `check.mjs` | the checker |

What the checker does:

- `node check.mjs` catches:
  - syntax errors
  - a missing default export
  - `gb.*` calls that don't exist, with the right name suggested
  - blocked browser APIs (storage, dialogs, your own renderer or loop)
  - npm imports and missing files
  - file limits
- `node check.mjs --play` opens the game headless inside GrowBlocks. It reports every runtime error with its line and saves `check-screenshot.png`, so an agent can see its own result.

With a chat assistant (no file access), use the prompt on `devkit.html`. It points the model at `llms.txt` and the rules; save its answer as `game.js`.

In a local or test build, errors appear in a panel over the game, with a **Copy for your AI** button.

## From the terminal and AI assistants: the GrowBlocks CLI

`npx growblocks` (the npm package `growblocks`, also copied into every game folder as `growblocks.mjs`) is the GrowBlocks CLI and MCP server. Node 18 or later, nothing else to install.

```
npx growblocks login                           # once per computer: opens cli-login.html, you confirm a code
npx growblocks new my-game                     # a new folder from the latest starter kit
npx growblocks dev                             # serve it (CORS on): http://localhost:5173/play.html
npx growblocks check --play                    # the checker, with a headless play and a screenshot
npx growblocks create my-game --name "My Game" # make the game and link this folder (growblocks.json)
npx growblocks push                            # check, upload a test build, print its test link
npx growblocks submit                          # send it to GrowBlocks for review
npx growblocks status                          # builds, links, review state and note
```

`npx growblocks help` lists every command (also `games`, `link`, `update --logo logo.webp`, `update --studio "My Studio"` (the name players see after "by" instead of your account name), `open`, `docs <topic>`, `logout`).

**MCP.** `.mcp.json` in every game folder runs `node growblocks.mjs mcp`, so Claude Code and Cursor opened in the folder get these tools: `growblocks_docs`, `growblocks_check` (returns the screenshot), `growblocks_status`, `growblocks_games`, `growblocks_create_game`, `growblocks_link_game`, `growblocks_update_game`, `growblocks_push`, `growblocks_submit`, `growblocks_new_game`, `growblocks_login` and `growblocks_whoami`. Elsewhere: `claude mcp add growblocks -- node /path/to/growblocks.mjs mcp`.

**Sign-in.** `login` shows a code and opens `cli-login.html`; you check the code, sign in with Google and press **Allow**. The CLI gets its own token (in `~/.growblocks/credentials.json`, 90 days, `logout` revokes it). That token can only manage your games, builds and reviews. Collections, the coin, earnings and claims stay in your browser on `devkit.html`. For CI, set `GROWBLOCKS_TOKEN`.

## Shipping it

**From the terminal** (or ask Claude: "push a test build"):

```
npx growblocks login                              # once per computer
npx growblocks create my-game --name "My Game"     # once: the game's address and name
npx growblocks push                               # check, upload a test build, print its test link
npx growblocks update --logo logo.webp --description "..."   # a logo and a description are needed to submit
npx growblocks submit                             # send it to GrowBlocks for review
```

**In the browser** it is the same, step by step:

1. In `devkit.html`, sign in with Google, then create the game: its address (`game.html?g=<address>`), name, tagline, description, logo and players per room.
2. **Upload build folder.** Pick the folder that has `game.js` at its top level. Limits: 200 files, 3 MB per file, 30 MB per build. Served file types: js, mjs, json, css, txt, png, jpg, webp, gif, svg, glb, gltf, bin, mp3, ogg, wav, woff2.
3. **Play test build.** The test link is unlisted: share it with testers. Multiplayer works in test builds; the chain does not yet.
4. **Submit for review.** GrowBlocks plays it and approves it, or sends a note. Once approved, the game is listed and playable at `game.html?g=<address>`.
5. **Updates are reviewed too.** Upload the new build, play its test link, then Submit update for review. Players keep the approved version until GrowBlocks approves the new one. GrowBlocks can take a game down at any time.

## Your game's NFTs and coin, and what you earn

### Who owns what

| | Belongs to | Linked to |
|---|---|---|
| Your game's code, art and sound | **you** | |
| Your game's NFT collections | **you**: you create them from your own GrowBlocks game wallet, and the chain records you as their creator | **your game** |
| Each NFT (piece) | **the player** who mints or buys it, in their own wallet | your game's collection |
| Your game's coin | **you**: you launch it from your own game wallet, and the chain records you as its creator | **your game** |
| The engine, the factory that creates collections, the market, the launchpad and the screens players pay through | GrowBlocks | |

"Linked to your game" means GrowBlocks files the collection or the coin under your game (its plaza, `game:<your-address>`). That is what puts them at your game's gate in the **GrowBlocks Games plaza** and next to your game in the market and on the launchpad, and what lets your code read them with `gb.chain` (below). Only you, as your game's developer, can create collections or launch a coin for it; nobody else can file theirs under your game.

- **Collections:** open your game, then Collections → Create. You choose the pieces, the price and currency, the supply and a royalty of 0 to 10%.
- **The coin:** first choose its name and ticker on your game's card in the Dev Kit (**Game coin**). GrowBlocks holds them for your game: nobody else can launch a coin with that name or ticker. Then launch it whenever you like, on GrowBlocks (not inside your game): in the main GrowBlocks plaza, open **LAUNCHPAD** at the top of the screen, go to **Create a coin**, and pick your game under **Launch for plaza**. The name and ticker you hold fill in by themselves. Your game's code does not need anything for this.

### What you earn

> ⚠️ **Not on mainnet yet.** The Dev Kit, your earnings and every payout run on **Robinhood Testnet only**, in **tPLAY test credits that have no value**. The mainnet rates will be confirmed before mainnet opens.

**One rule, the same for everyone on GrowBlocks: of every fee your game's NFTs and coin generate, 70% is yours.** 10% goes back to players (5% Shooter prizes, 3% invite rewards, 2% plaza rewards) and 20% to GrowBlocks. Players pay nothing extra.

| Every fee | Share |
|---|---|
| You (the game's developer) | **70%** |
| Players: Shooter prizes, invite and plaza rewards | 10% |
| GrowBlocks | 20% |

The fees are the same as everywhere in GrowBlocks: **1%** of every trade of your coin, **2%** of every mint of your NFTs, and **2%** of every market sale of your NFTs.

- **Your coin:** the launch contract pays you your 70% of every trade directly (60% + the 10% drops share). Nothing to claim from GrowBlocks.
- **Your NFTs:** the collection pays you the drops share of its mint fee (a quarter of it) directly, and GrowBlocks pays you the rest of your 70% of the mint and resale fees. Claim it in the portal (**Earnings**); the payouts contract (`GrowBlocksDevPayouts`) pays it to your GrowBlocks game wallet.

**Examples:**

| Players... | Fee | Straight to you | From GrowBlocks | You get (70%) | Players (10%) | GrowBlocks (20%) |
|---|---|---|---|---|---|---|
| trade $1,000 of your coin | $10 | $7.00 | $0 | **$7.00** | $1.00 | $2.00 |
| mint $100 of your NFTs | $2 | $0.50 | $0.90 | **$1.40** | $0.20 | $0.40 |
| resell $100 of your NFTs | $2 | $0 | $1.40 | **$1.40** | $0.20 | $0.40 |

**On top of the fees, and yours in full:**

- **Mint sales:** the $98 of every $100 mint (the price minus the 2% fee). Withdraw it from your collection whenever you like.
- **Royalties:** the royalty you set (0 to 10%) on every resale of your NFTs.
- **Creator tax:** if you set one when you launch your coin (up to 2% per trade).

**The drops share is yours to keep or to give.** The 10% drops share of your coin's fee and the drops share of your mint fee are meant for your community: keep them, or use them for drops and rewards to your players. Either way they count as part of your 70%.

The fees are read from the chain (`server/devkit-revenue.cjs`), per currency.

## Blocks: build on each other's work, and earn from it

A **block** is a part of a game that you publish for other games to use: a kart with its physics, a game mode, an enemy brain, a level kit, a HUD. Games that ship your block pay you, and you never have to sell it.

```
npx growblocks blocks racing          # find blocks
npx growblocks add kart-physics       # copy one into this game: blocks/kart-physics/
npx growblocks block init my-block    # start your own: block.json + index.js
npx growblocks block publish          # publish it (immutable versions)
npx growblocks block status           # yours, and the live games that ship them
```

**Using a block.** `add` copies its files into `blocks/<block>/` and records it in `growblocks.json`. Import it like your own files (`import * as kart from './blocks/kart-physics/index.js'`). Keep its files unchanged. When you upload a build, GrowBlocks recognises every block in it by its files, and `push` tells you which. Using blocks costs you nothing: your share stays 70%.

**The catalogue** is https://growblocks.xyz/blocks.html: every live block, the live games that ship it, its code. Ship a small `preview.webp` (or .png/.jpg, 16:9) in your block to show a picture there.

**Publishing a block.** A block is a folder with `block.json` (`slug`, `name`, `description`, `entry`, `tags`) and its files: at most 60 files, 2 MB each, 10 MB in all, the same file types as games. Write blocks to take `gb` as a parameter (`export function setup(gb, options)`), never as a global. Each publish is a new, permanent version; games keep the version they added. A block whose files are mostly someone else's block is refused unless `block.json` says `"forkOf": "their-block@v3-…"`.

**What blocks earn** (Robinhood Testnet, tPLAY, until mainnet):
- Of all the fees a game makes while its **live** build ships blocks, **5% goes to those blocks**, split evenly between them. GrowBlocks pays it out of its own 20%: the game's developer keeps 70% and players 10%.
- A **fork** keeps 70% of its part and passes 30% up to the block it started from, and so on up to 3 levels. Build on someone's work and you both earn.
- Earnings start when a game with your block goes live, follow its live build (an update without your block stops them; what you earned stays), and only ever grow. They are added to your Dev Kit earnings and claimed the same way (devkit.html, Earnings), to your game wallet.

| A game's fees | Game developer | Players | Its blocks | GrowBlocks |
|---|---|---|---|---|
| $1,000 | $700 | $100 | $50 | $150 |

With one forked block in that game, the fork's author gets $35 and the original author $15.

## How your game runs

Your code runs in a sandbox: `devkit/runtime.html`, framed by `game.html`.

- The sandbox has no origin of its own. It cannot read GrowBlocks' storage, cookies or wallet, and it cannot reach the page around it.
- Everything touching money goes through `gb.chain`. `gb.chain` asks the page around the game, which opens GrowBlocks' own screens, and those screens use GrowBlocks contracts.
- Your game can never move a player's funds, and players can see that every mint or sale happens on a GrowBlocks screen.

`game.js` must export a default function. It receives `gb`:

```js
export default async function game(gb){
  gb.world.style('sunset');
  gb.world.ground({size:60});
  gb.player.spawn({position:[0,1,6]});
}
```

You can import `three` and `three/addons/...` directly. The versions are the same ones `gb` uses. You can also import your own files with relative paths (`import {level} from './level.js'`).

## The `gb` API

### Engine

| | |
|---|---|
| `gb.THREE`, `gb.RAPIER` | the libraries (three 0.170, Rapier 0.14) |
| `gb.scene`, `gb.camera`, `gb.renderer` | the three.js objects; add anything to the scene |
| `gb.sun`, `gb.hemi` | the lights |
| `gb.onUpdate((dt, t) => {})` | runs every frame; returns a function that stops it |
| `gb.material(color, {glow, metal, rough})` | the GrowBlocks look |
| `gb.me` | `{name, look, signedIn, address}` |
| `gb.game` | `{slug, name, mode}`, where mode is `live`, `test` or `dev` |

### World

| | |
|---|---|
| `gb.world.style(name)` | `day`, `sunset`, `night`, `neon` or `space` |
| `gb.world.ground({size, color})` | a floor with physics |
| `gb.world.box({size:[w,h,d], position, rotation, color, glow, physics, mass, bounce})` | `physics` is `'fixed'`, `'dynamic'` or `false` |
| `gb.world.sphere({radius, position, color, physics, mass, bounce})` | a ball |

### Physics (Rapier; gravity 20 m/s², 60 steps a second)

| | |
|---|---|
| `gb.physics.add(mesh, {type, shape, mass, bounce, friction, sensor})` | gives any mesh a body; `type` is `dynamic`, `fixed` or `kinematic`; `shape` is `box`, `sphere` or `capsule` |
| `gb.physics.push(mesh, [x,y,z])` | an impulse |
| `gb.physics.velocity(mesh, [x,y,z])` | sets the velocity |
| `gb.physics.place(mesh, position)` | moves a body |
| `gb.physics.remove(mesh)` | removes its body |
| `gb.physics.onHit(mesh, other => {})` | called when this mesh starts touching something |
| `gb.physics.raycast(from, dir, max)` | returns `{mesh, point, distance}` or `null` |
| `gb.physics.gravity` | read or set it |
| `gb.physics.world` | the raw `RAPIER.World`, for anything else |

A kinematic mesh follows its `mesh.position`. A dynamic mesh is moved by the simulation.

### The player

| | |
|---|---|
| `gb.player.spawn({position, camera})` | your GrowBlocks character: WASD or the touch stick, Shift to run, Space to jump. `camera` is `'third'`, `'first'` or `false` (you drive `gb.camera` yourself) |
| `gb.player.position`, `gb.player.velocity` | where it is and how it moves |
| `gb.player.yaw`, `gb.player.pitch` | where it looks |
| `gb.player.grounded` | whether it is standing on something |
| `gb.player.speed`, `gb.player.sprint`, `gb.player.jump` | its movement settings |
| `gb.player.enabled = false` | freezes it (for cut-scenes and menus) |
| `gb.player.teleport(position)` | moves it instantly |
| `gb.avatar(name, look)` | another GrowBlocks character: `.moveTo(x,y,z,yaw)`, `.place()`, `.say(text)`, `.remove()`, `.group` |

### Your own characters

Any rigged GLB/GLTF with animation clips can be the player, an NPC or everyone in a room:

```js
const hero = await gb.assets.character('models/hero.glb', {height: 1.8});   // or a cdn.jsdelivr.net URL
gb.player.spawn({position: [0, 1, 6], character: hero});                    // our movement, camera and physics
const guard = gb.avatar('Guard', {}, {character: hero});                     // an NPC with the same model
room.showPlayers({character: hero});                                         // everyone drawn as it
room.showPlayers({characters: {knight, robot}, pick: 'knight'});             // each player picks; room.pick('robot')
```

- **Options:**
  - `height` scales the model, in metres.
  - `scale` is used when `height` is not given.
  - `facing` turns a model that doesn't face +Z, in radians; try `Math.PI`.
  - `clips: {idle, walk, run, jump}` names the clips exactly. When it's left out, they are found by name.
  - `runAt` is the speed at which walking becomes running.
- **Sizing:** the physics capsule, camera height and name tag all fit the model.
- **Animations:** the idle, walk, run and jump clips play by themselves. If a clip name is wrong, the error lists the clips the file actually has.
- **Where to get models:** Mixamo (exported as GLB), Quaternius, Kenney, or your own Blender export.
- **A test model:** RobotExpressive, CC0, from the three.js examples. See `devkit/examples/robots/game.js`.
- **Review:** characters are checked too. No copyrighted characters.

### Input

| | |
|---|---|
| `gb.input.held('KeyW')` | true while the key is down |
| `gb.input.pressed('KeyE')` | true only on the frame it went down; `'Mouse0'` is a click |
| `gb.input.move` | `{x, y}` from the keys or the stick |
| `gb.input.action` | the fire/act button |
| `gb.input.mouse` | `{dx, dy, buttons, wheel}` |
| `gb.input.touch` | true on phones |
| `gb.input.lockOnClick` | whether a click locks the mouse (default `true`) |

### HUD and sound

| | |
|---|---|
| `gb.ui.toast(text, ms)` | a short message |
| `gb.ui.text(id, html, where)` | a text block, placed `tl`, `tr`, `bl`, `br`, `top` or `center` |
| `gb.ui.clear(id)` | removes a text block |
| `gb.ui.root` | the HUD layer, for your own DOM |
| `gb.sound.play(kind, volume)` | `kind` is `coin`, `jump`, `hit`, `win` or `click` (synthesized, no files), or a clip you loaded |
| `gb.sound.load({name: 'sfx/a.mp3'})` | your own clips from your build (SDK 1.3); also `loop`, `music`, `duck`, `say` (see Sports games) |
| `gb.ui.canvas()`, `gb.ui.fonts()` | a drawn HUD canvas and the GrowBlocks typefaces (SDK 1.3) |
| `await gb.ui.share({text, url})` | share to X or Telegram (SDK 1.6): GrowBlocks shows its own share sheet. `url` defaults to your game's GrowBlocks share card; a path on growblocks.xyz is also allowed. Resolves `{ok, via, url}`, `via` being `'x'`, `'telegram'`, `'copy'` or `null` |

### Assets

| | |
|---|---|
| `gb.assets.url(path)` | a file from your build |
| `await gb.assets.gltf(path)` | a GLTF model |
| `await gb.assets.texture(path)` | a texture |
| `await gb.assets.json(path)` | a JSON file |

Load each file from the path it ships at. A load rejects when the file is missing, so wrap an optional asset in `try/catch` and fall back rather than checking first. Published, GrowBlocks serves your files like a static site (GET and HEAD). `node check.mjs --play` serves them the same way and lists every file your game asked for and did not get; fix each one, because the published game would miss it too.

### Saved progress

| | |
|---|---|
| `await gb.store.get(key)` | reads a value this player saved in your game |
| `await gb.store.set(key, value)` | saves a value on the player's device |

Up to 64 KB per game.

### Multiplayer

```js
const room = await gb.net.join();   // the first room of your game with space, or gb.net.join({room:'red'})
room.showPlayers();                 // everybody's character, in everybody's game
room.on('join', p => {});  room.on('leave', p => {});
room.on('message', (data, from) => {});
room.send({type:'shot', x:1});      // to everybody else
room.sendTo(id, data);              // to one player
room.state.set('score:'+room.id, 7);  room.on('state', (key, value, by) => {});
room.isHost;  room.on('host', id => {});
room.players;  room.id;  room.leave();
```

- **Rooms:** hold up to your game's players-per-room setting (16 at most). A full room sends newcomers to a new one.
- **Messages:** up to 8 KB each, about 40 a second per player.
- **Shared state:** up to 256 keys and 64 KB. A player who arrives late gets the state as it stands.
- **The host:** the first player in a room is its host, and hosting passes on when they leave. Use the host as the single source of truth for anything that must agree, such as spawning, scoring or a shared physics world.
- **Trust:** the server relays messages; it does not referee them. Treat other players' messages as untrusted input.

### Chain

Every call that spends or moves something opens a GrowBlocks screen; none of them move funds silently. `earn()` only ever gives the player an item, so it asks once per game and then stays out of the way.

| | |
|---|---|
| `await gb.chain.balance()` | `{signedIn, address, tPLAY, ETH}` |
| `await gb.chain.collections()` | your game's collections |
| `await gb.chain.items()` | the pieces this player holds from your game |
| `await gb.chain.earn(itemKey, {rule})` | the player earned an item (SDK 1.5): minted free to their game wallet, gas paid for them. See "Items players earn" below |
| `await gb.chain.mint(collection, {qty})` | opens the GrowBlocks mint screen on that collection; resolves with `{ok, minted:[{collection, tokenId}]}` |
| `await gb.chain.market()` | the shop for your game's pieces (buy, sell, list); resolves when the player closes it |
| `await gb.chain.trade(address, {name})` | the GrowBlocks trade window with another player (SDK 1.4): both pick pieces or coins and sign one swap. `address` is a wallet that player shared through `balance()` and your game sent over `gb.net`; no address opens the Trade tab |
| `await gb.chain.coin()` | your game's coin screen |
| `await gb.chain.token()` | your game's coin: `{name, symbol, status, address, tradable}`, `status` `'none'`, `'held'`, `'launching'` or `'live'`. No screen |
| `await gb.chain.tokenBalance()` | how much of your coin the player holds: `{signedIn, shared, symbol, amount}`. Asks once, like `balance()` |
| `await gb.chain.holds({collection, minimum})` | `true` when the player owns at least `minimum` of your game's pieces (or of one collection). No screen |
| `gb.chain.onChange(fn)` | `fn({op})` after a mint, market, trade or coin screen closes, so you can refresh what the player owns |

Errors carry a `code`: `not-live` (a test build), `signin`, `wallet` or `trade-invalid`.

### Your game's coin and NFTs (SDK 1.1)

Your code never names, launches or pays with a coin. GrowBlocks holds your coin's name, launches it from your game's plaza, and opens its own screens for every payment, so players can trust every game, and the fees (70% of them yours) are always counted. What your game does is read and react:

```js
export default async function game(gb){
  // ...your world and player...

  // The coin: show it in the HUD, and let players open its trading screen once it is launched.
  let token={status:'none'};
  async function showCoin(){
    token=await gb.chain.token();
    let label='';
    if(token.status==='live'||token.status==='launching'){
      const b=await gb.chain.tokenBalance().catch(()=>null);
      label='$'+token.symbol+(b?.shared?' · '+b.amount:'');
    }else if(token.status==='held')label='$'+token.symbol+' · coming soon';
    gb.ui.text('coin',label,'tr');
  }

  // NFTs: owning a piece of your collections unlocks something in the game.
  let collector=false;
  async function applyPerks(){
    const now=await gb.chain.holds({minimum:1}).catch(()=>false);
    if(now&&!collector)gb.ui.toast('Collector perk unlocked');
    collector=now;
  }

  // Buttons for touch screens (your own DOM in gb.ui.root), and keys for keyboards.
  const shop=gb.ui.text('shop','<button data-coin>COIN</button> <button data-market>MARKET</button>','br');
  const open=async fn=>{try{await fn();}catch(e){gb.ui.toast(e.message);}};
  shop.querySelector('[data-coin]').onclick=()=>open(()=>gb.chain.coin());
  shop.querySelector('[data-market]').onclick=()=>open(()=>gb.chain.market());
  gb.onUpdate(()=>{
    if(gb.input.pressed('KeyC'))open(()=>gb.chain.coin());
    if(gb.input.pressed('KeyM'))open(()=>gb.chain.market());
  });

  // After any GrowBlocks mint, market or coin screen closes, refresh.
  gb.chain.onChange(()=>{showCoin();applyPerks();});
  showCoin();applyPerks();
}
```

In a local build every call is simulated (`token()` says `'live'` with the ticker `LOCAL`), and in a test build `token()` shows the real status while trading waits for approval.

### Your pieces on the GrowBlocks plaza, and trades inside your game (SDK 1.4)

You create your collections and your coin from your game's plaza (Collections → Create; the coin from the Dev Kit page). After that they work like any other GrowBlocks collection or coin:

- **On the plaza.** Your collections are listed in the GrowBlocks plaza's Collections panel next to everyone else's. Mints there count the same as mints in your game. Your pieces can be listed and bought in the plaza shop, swapped in the plaza trade window, and dropped on the plaza floor wherever drops are open. Your coin trades in the Launchpad and can be the coin side of any trade.
- **In your game.** `gb.chain.mint(collection)` opens the mint screen on that collection, and `gb.chain.market()` opens your game's shop. `gb.chain.trade(address)` opens the same trade window players use on the plaza: both pick pieces or coins and sign one swap, and nothing moves until both have signed.
- **Fees.** These are the same everywhere, and every one counts for your game whether the sale happened in the game or on the plaza: 2% of a mint, 2% of the coin side of a shop sale or a trade (shared by count when a coin pays for your pieces and other pieces together), and 1% of a coin trade. 70% of all of them are yours (Earnings in the Dev Kit page).

Two players of your game trade like this. Each player shares their address once (GrowBlocks asks them), and your game keeps it in the room's shared state, so players who join later see it too:

```js
const room=await gb.net.join();room.showPlayers();
const b=await gb.chain.balance().catch(()=>null);
if(b?.shared)room.state.set('wallet:'+room.id,b.address);// only players who agreed to share it
async function tradeWith(id){
  const address=room.state.get('wallet:'+id);
  if(!address)return gb.ui.toast('That player has not shared their wallet with this game.');
  try{await gb.chain.trade(address,{name:room.players.get(id)?.name});}catch(e){gb.ui.toast(e.message);}
}
```

An address from another player is only an invitation: the trade window shows both players' real pieces read from the chain, and each player signs what they see.

### Items players earn (SDK 1.5, testnet)

Some items should be earned, not bought: a trophy for the first Gorbo kill, a rare find in a chest, a badge for finishing the tutorial. An **earned collection** has no price and no sale window. An item exists only because a player earned it in your game, and each earned moment mints once.

**1. The collection.** Create it on the Dev Kit page: in your approved game's card open **Earned items**, name the collection, pick the pieces players can earn from your Studio library, and sign once with your game wallet (gas is paid for you). The page then shows a `rewards.json` to start from. Each design has an `itemKey` (your own id, e.g. `gorbos_clipboard`), a kind (object, wearable with a slot, or art), a `cap` (0 = open-ended), `perWallet` (default 1) and `soulbound`. Soulbound designs (badges) stay with the player who earned them, enforced on chain (ERC-5192). Everything else is a normal GrowBlocks piece: it shows in My pieces, sells in the shop, trades in the trade window, drops on the plaza floor, and pays your royalty on every resale. You can add designs later (a new floor, a new boss) and close one so it mints no more; a design never changes once it exists.

**2. The rules, in your build.** Ship a `rewards.json` next to `game.js`. It goes through review with the build, and GrowBlocks refuses any rule that is not in your live build:

```json
{ "rules": {
  "first_kill:gorbo": { "item": "gorbos_clipboard", "once": "per_player", "minSession": 300 },
  "chest_find":       { "items": ["mimics_tooth", "gilded_hard_hat"], "perDay": 1 }
} }
```

Every rule sets exactly one of `"once": "per_player"` or `"perDay": n` (1 to 20). `minSession` is the seconds the player must have been playing (0 to 7200). Players are counted by GrowBlocks account, not by wallet.

**3. In the game.**

```js
const r=await gb.chain.earn('gorbos_clipboard',{rule:'first_kill:gorbo'});
if(r.status==='minted'||r.status==='pending')showItemEarned('Gorbo's Clipboard');
gb.chain.onChange(async({op})=>{if(op==='earn')inventory=await gb.chain.items();});
```

The first `earn()` in your game asks the player once ("ROGBLOX wants to send items to your game wallet"); after that, earning is silent. `status` is `minted`, `pending` (it mints later: the player has no game wallet yet, or your game is out of gas), `already` (earned before), `denied` (rule or item not allowed), `declined`, `signin` or `simulated` (local builds, and other players on your test link). Read what a player holds with `gb.chain.items()` and match on `itemKey`.

**Testing before review.** On your game's test link, you (signed in as the game's developer) earn real testnet items from the uploaded build's `rewards.json`, so you can play the whole loop, see the item in My pieces and trade it. Test earns count per uploaded build (upload again to test "first kill" again), up to 20 a day, and they never use up your real earns. Anyone else on your test link gets `simulated`.

**Gas.** GrowBlocks pays the gas for your game's earns up to a free monthly allowance. Past it, gas comes from your game's **gas tank**: fill it from the Dev Kit page, set a daily limit (0 pauses earns), and take back what you have not used at any time. GrowBlocks can never withdraw it. When neither can pay, earns wait as `pending` and mint after your next top-up; players never lose an item.

**Trust.** A game runs in the player's browser, so a player with dev tools can call `earn()` too. The limits above (once per account, per day, caps, minimum session, review of `rewards.json`) bound what that is worth. Keep items with real market value capped, and expect GrowBlocks to ask for server-side attestation for valuable tradeable designs before mainnet.

### AI decisions with Sage (SDK 1.2)

`gb.ai` gives your game typed decisions from **Sage by Levanto Labs**: yes/no, pick one of several options, score on a five-level rubric, rank a list, or tag. Use it for map and level generation, enemy and combat AI, NPC behaviour, difficulty and moderation. GrowBlocks calls Sage for your game with its own key, so there is no key in your code and nothing to pay while you build.

| | |
|---|---|
| `await gb.ai.yesno(content, instructions)` | `{answer: 'yes' \| 'no' \| null, probability}` |
| `await gb.ai.choice(content, instructions, options)` | `{chosen, probability, probabilities}`; options are strings or `{option, description}` |
| `await gb.ai.scale(content, instructions, levels)` | `{expectation, confidence}` on 0-4; `levels` is five strings, level 0 first |
| `await gb.ai.sort(items, instructions)` | `{sorted, confidence}`: item ids, first = most |
| `await gb.ai.tags(content, tags, instructions)` | `{tags: [{id, probability, applies}]}` |
| `await gb.ai.decide(content, questions)` | several questions about the same content in one call |
| `await gb.ai.decideGroups(groups)` | many contents in one call |

`content` can be text or any JSON value, such as your game state. Add `{reasoning: 'off'}` as the last argument for the fastest answers (about 0.1-0.3 s), which suits combat; the default `'auto'` thinks through hard questions (up to a few seconds), which suits level generation.

```js
// An enemy that decides every 1.5 seconds and acts on its plan every frame.
let plan='chase',thinking=false;
async function think(){
  if(thinking)return;thinking=true;
  try{
    const r=await gb.ai.choice({enemy:{hp:enemyHp},player:{hp:playerHp,distance:dist}},'You are the enemy. What next?',
      [{option:'chase',description:'close in and attack'},{option:'flank',description:'circle around'},{option:'retreat',description:'back off to heal'}],{reasoning:'off'});
    plan=r.chosen??plan;          // null: Sage is not sure, keep the last plan
  }catch(e){plan='chase';}        // no AI right now: a plain rule
  finally{thinking=false;}
}
setInterval(think,1500);
```

Rules of thumb:

- Never call `gb.ai` every frame. Decide per turn, per wave, per spawn or every second or two, and act on the last decision in `gb.onUpdate`.
- A `null` answer is Sage saying it is not sure. Use your own default; do not retry.
- Always catch errors and fall back to plain game logic. `err.code` is `ai-allowance` (this month's allowance is used up), `ai-rate` (too many a minute), `ai-unavailable`, `ai-invalid` or `ai-dev-key`.
- Each game has a monthly allowance of decision units (one per question; long content counts more), larger once it is live. You see your use on devkit.html under **AI decisions**.
- On your own computer, `gb.ai` calls the real Sage with your **dev key**: on devkit.html, AI decisions, **Make a dev key**, and save it as `growblocks.key` next to `play.html`. Never commit it; the Dev Kit never uploads it.
- Do not call Sage or any other AI API yourself: the sandbox blocks it, and a key in game code is public.

### Sports games: the kit behind SLAM BLOCKS (SDK 1.3)

SLAM BLOCKS, GrowBlocks' 2v2 arcade basketball, runs on the Dev Kit like any game, and everything it is made of is yours to use. Its whole source is an example you can read and copy: [devkit/examples/slam/](examples/slam/game.js) (`game.js` the game, `court.js` the hall, `rules.mjs` the numbers). Start from it for basketball, football, hockey, volleyball, dodgeball, tennis, golf.

| | What it gives you |
|---|---|
| `gb.sports.athlete()` | A voxel athlete posed in code every frame: runs that match the ground covered, defensive slides, dribbles, jump shots, blocks, six dunks, a rim hang, knockdowns and getting back up, steals and shoves. Feet stay on the floor; it leans with real acceleration. |
| `gb.sports.effects()` | Dust, sparks, bursts, confetti, fire trails, swipes, floor shockwaves, and a glass panel that explodes into cubes and flies back together. Instanced: thousands of pieces, no garbage. |
| `gb.sports.crowd()` | Stands full of fans who bounce harder the louder it gets (`cheer`) and fire camera flashes (`flash`). |
| `gb.sports.arc()` / `landing()` | The launch velocity that lands a ball where you want in the time you want; where a flying ball will come down. |
| `gb.time` | Slow motion (`slow`) and hit-stop (`hitstop`) for the big moments, on the game clock only. |
| `gb.render` | Bloom and your own post passes. |
| `gb.input.controls()` | Named actions for 1-4 players on one screen: keyboard, pads, split keyboard and the phone at once. |
| `gb.input.touchControls()` | Phone buttons that change with the play (SHOOT / DUNK / BLOCK), a stick that turbos at its edge, a timing ring. |
| `gb.input.pads`, `rumble()` | Controllers by button name; a buzz in the hands. |
| `gb.sound.load/loop/music/duck/say` | Your own clips, crowd loops, looped music with loop points, ducking and an announcer that never talks over itself. |
| `gb.ui.canvas()`, `fonts()` | A drawn HUD canvas and the GrowBlocks typefaces. |
| `await gb.ui.share({text, url})` | A GrowBlocks share sheet: X, Telegram or Copy. A game cannot open links itself. |
| `room.stream()`, `sendControls()` | Online the way SLAM BLOCKS does it: the host runs the game and streams 20 snapshots a second, everybody else sends presses and draws 110 ms behind. |
| `gb.score` | Leaderboards. |

The full reference with code is in [llms.txt](llms.txt), "Sports games and arcade feel".

Leaderboards (`gb.score`): `await gb.score.submit(points, {board:'main'})` keeps each player's best and returns `{saved, best, rank}`; `gb.score.top({board})` lists the board. Saving needs a signed-in player: catch `err.code === 'signin'` and offer a button that calls `gb.score.signIn()`. Use `order:'low'` on boards where less is better (times), on every submit. Boards are for fun: scores come from the game, so GrowBlocks pays no XP or prizes for them and removes cheats. Local builds keep simulated boards in your browser; test builds have their own boards, separate from the live game's.


## Rules

- Your game is reviewed before it goes live, and can be taken down. No scams, no impersonation, nothing that asks players for keys or seed phrases.
- The chain is testnet (Robinhood Chain testnet, tPLAY) during the beta.
- GrowBlocks' fees and your share (70% of every fee) are the same for every game.
