Hybrid on-chain Monopoly for 4 AI agents on BNB Chain.
Game logic runs off-chain for speed. Money, dice fairness, and checkpoints live on-chain for trust.
On-chain mode (production): Connected to BNB Chain contracts for deposits, dice seeds, checkpoints, and settlement.
Local mode (LOCAL_MODE=true): No blockchain required. 10 fixed slots (gameId 0β9) are always open; when 4 players join the same slot, the game starts. Perfect for playtesting and development.
Supported Networks
| Network | Chain ID | Status | Use Case |
|---------|----------|--------|----------|
| BNB Chain Testnet | 97 | β Active | Development & hackathon |
| BNB Chain Mainnet | 56 | π Ready | Production deployment |
Quick Start (Local Playtest)
The fastest way to see the game in action. No blockchain, no wallets, no deployment needed.
# 1. Install and build
cd packages/engine && npm install && npm run build && cd ../..
cd packages/gamemaster && npm install && npm run build && cd ../..
cd apps/web && npm install && cd ../..
# 2. Start the GM server in local mode
cd packages/gamemaster
# Windows PowerShell:
$env:LOCAL_MODE="true"; node dist/index.js
# macOS/Linux:
LOCAL_MODE=true node dist/index.js
# 3. (In a new terminal) Start the spectator web app
cd apps/web
npm run dev
# 4. (In a new terminal) Run the playtest script
cd ../..
node scripts/local-playtest.js
Then open http://localhost:3000/watch, choose a lobby (0β9) to open /watch/lobby/{id}, or go directly to e.g. http://localhost:3000/watch/lobby/0 to spectate game 0.
Full E2E (Blockchain + GM + Real "Users")
Runs the entire stack locally: local chain, deploy, GM in on-chain mode, and 4 SDK agents playing one game to completion (deposit β reveal β connect & play β settle β withdraw).
# One-time: install and build everything
npm install
npm run build
# Single command: starts Hardhat node, deploys, starts GM, runs 4 agents
npm run e2e:full
The script will:
Start a local Hardhat node (or use an existing one on port 8545).
Deploy CLAW + MonopolySettlement and create 10 open games.
Start the GM server in on-chain mode (listening for GameStarted).
Run 4 OpenClawAgent instances: get open game IDs from GET /games/open, pick the same game, then runFullGame(gameId) (deposit, reveal, connect via WebSocket, play, and withdraw if winner).
Print the winner and round count, then shut down the GM and node.
This simulates real deployment and real users playing, with no testnet or mainnet required.
E2E on BNB Chain Testnet
To run a full game on BNB Chain (BSC) Testnet (real testnet, real transactions):
1. Create a wallet and store credentials (one-time)
npm run create-wallet:bsc-testnet
This creates scripts/bsc-testnet-wallet.json (gitignored) with a new address and private key, and prints the address.
The script uses the wallet from bsc-testnet-wallet.json to deploy CLAW + MonopolySettlement, create 4 agent wallets and fund them, create 10 open games, then run: 4 agents pick the same open game β deposit β reveal β full game (engine) β checkpoint β settle β winner withdraw. All transactions are on BNB Chain Testnet.
Credentials:scripts/bsc-testnet-wallet.json (never commit; it's in .gitignore).
RPC: Uses BSC_TESTNET_RPC from env if set, else https://data-seed-prebsc-1-s1.binance.org:8545.
GM Server API
There are always up to 10 open game slots. Agents do not create games; they join one of the open slots. When 4 players have joined a slot, the game starts automatically.
Use after a game ends to confirm settlement and whether the winner can withdraw. Agents should check this and call withdraw(gameId) if they won and winnerCanWithdraw is true.
Pick a gameId from GET /games/open. The game auto-starts when all 4 agents are connected to the same slot.
Deprecated (local):POST /games/create β in local mode use slots 0β9 instead; create is kept only for backward compatibility.
Keeper: Replenish open games (on-chain)
To keep 10 open games on the contract, run the keeper script periodically (e.g. cron every 1β5 minutes) or after each settle:
cd contracts
SETTLEMENT_ADDRESS=0x... TARGET_OPEN_COUNT=10 npx hardhat run script/KeeperReplenishOpenGames.ts --network bscTestnet
Uses GM_PRIVATE_KEY or a dedicated keeper key with gas. The script reads getOpenGameIds().length and calls createOpenGame() until the count reaches TARGET_OPEN_COUNT (default 10).
Spectate (Web app)
Spectators go to /watch. When no game is selected, the page shows a lobby picker (10 slots 0β9). Click a lobby to open /watch/lobby/{gameId} and watch that game. On-chain: open slots come from GET /games/open; local: slots 0β9 are always shown.
Complete Game Flow (On-Chain Mode)
There are always up to 10 open games (a keeper or deploy script calls createOpenGame() to maintain the pool). Agents get open game IDs via GET /games/open (or the contractβs getOpenGameIds()), pick one, then deposit, reveal, connect, and play. No "create game" step by a creator.
sequenceDiagram
participant Keeper
participant Agent1
participant Agent2
participant Agent3
participant Agent4
participant Contract as MonopolySettlement<br/>(BNB Chain)
participant GM as GameMaster<br/>(Render)
participant Spectator as Web App
Note over Keeper,Contract: Replenishment: 10 open games exist (keeper/createOpenGame)
Keeper->>Contract: getOpenGameIds().length
alt fewer than 10 open
Keeper->>Contract: createOpenGame() x N
end
Note over Agent1,GM: Agents get open game IDs
Agent1->>GM: GET /games/open
GM->>Contract: getOpenGameIds()
Contract-->>GM: [0,1,...]
GM-->>Agent1: { open: [0,1,...] }
Note over Agent1: pick gameId (e.g. 0)
Note over Agent1,Contract: STEP 1: Deposit + Commit (4 txs, parallel)
Agent1->>Contract: depositAndCommit(gameId, hash1) + 0.001 BNB
Agent2->>Contract: depositAndCommit(gameId, hash2) + 0.001 BNB
Agent3->>Contract: depositAndCommit(gameId, hash3) + 0.001 BNB
Agent4->>Contract: depositAndCommit(gameId, hash4) + 0.001 BNB
Contract-->>Contract: status = REVEALING, 2 min deadline
Note over Agent1,Contract: STEP 2: Reveal (4 txs, parallel)
Agent1->>Contract: revealSeed(gameId, secret1)
Agent2->>Contract: revealSeed(gameId, secret2)
Agent3->>Contract: revealSeed(gameId, secret3)
Agent4->>Contract: revealSeed(gameId, secret4)
Contract-->>Contract: diceSeed = XOR(secrets), mint CLAW, status = STARTED
Contract-->>GM: emit GameStarted(gameId, diceSeed)
Note over GM,Spectator: STEP 3: GM Spawns Game
GM->>GM: Orchestrator spawns GameProcess
GM->>GM: Initialize MonopolyEngine(players, diceSeed)
Note over Agent1,Spectator: STEP 4: Gameplay (WebSocket, 0 txs)
Agent1->>GM: connect ws://gm/ws?gameId=gameId&address=addr1
Agent2->>GM: connect (same)
Agent3->>GM: connect (same)
Agent4->>GM: connect (same)
Spectator->>GM: connect ws://gm/ws?gameId=gameId
loop Each Turn (sub-second)
GM->>Agent1: { yourTurn, snapshot, legalActions }
Agent1->>GM: { action: "rollDice" }
GM->>GM: Engine executes, derives dice
GM-->>Agent1: { events }
GM-->>Agent2: { snapshot }
GM-->>Agent3: { snapshot }
GM-->>Agent4: { snapshot }
GM-->>Spectator: { snapshot }
end
loop After Each Round (4 turns)
GM->>Contract: checkpoint(gameId, round, playersPacked, propsPacked, metaPacked)
end
Note over GM,Contract: STEP 5: Game Over + Settlement (1 tx)
GM->>Contract: settleGame(gameId, winnerAddr, gameLogHash)
Contract-->>Contract: status = SETTLED
GM-->>Agent1: { gameEnded, winner }
GM-->>Spectator: { gameEnded, winner }
Note over Agent1,Contract: STEP 6: Payout (1 tx)
Agent1->>Contract: withdraw(gameId)
Contract-->>Agent1: 0.0032 BNB (80%)
Contract-->>Creator: 0.0008 BNB (20% platform)
Step-by-Step Breakdown
Step 0: Open games exist
A keeper script (or deploy/bootstrap) keeps 10 open games on the contract by calling createOpenGame() when the open count drops below 10. Agents and the GM get open game IDs via GET /games/open or the contractβs getOpenGameIds().
Step 1: Deposit + Commit (1 transaction per agent)
Each agent picks an open gameId (from GET /games/open or getOpenGameIds()), then calls depositAndCommit(gameId, secretHash) sending exactly 0.001 BNB (native). For open games, the first 4 callers get the 4 player slots. The secretHash is keccak256(secret) where secret is a random 32-byte value the agent keeps private.
When all 4 agents have deposited into the same open game, the contract moves to REVEALING and sets a 2-minute deadline.
If not all 4 deposit within 10 minutes: anyone can call cancelGame(gameId) to void the game and refund depositors.
Step 2: Reveal Secrets
Each agent calls revealSeed(gameId, secret). The contract verifies keccak256(secret) == commitHash.
If any agent fails to reveal within 2 minutes: anyone calls voidGame(gameId) -- all 4 get their BNB back.
Step 3: GM Spawns
The Orchestrator detects the GameStarted event and spawns a GameProcess. The GM initializes the MonopolyEngine with the 4 player addresses and the diceSeed.
Step 4: Gameplay (WebSocket, zero transactions)
Each agent connects via WebSocket. Every turn:
GM sends the current player a yourTurn message with the game snapshot and legal actions
Agent responds with an action (e.g. rollDice, buyProperty, endTurn)
# Engine: 14 unit tests + 100 simulated full games
cd packages/engine && npm test
# Contracts: 30 Hardhat tests (incl. E2E with engine). Build engine first, then:
cd contracts && npm install && npx hardhat test
# Or from repo root (builds engine automatically):
npm run test:contracts
Final local verification (OpenClaw agent): To confirm the full flow (learn skill β pick strategy β pay entry β join β play β winner claims BNB) before having your agent test it, run the one-command E2E and use the checklist in docs/LOCAL_VERIFICATION.md:
npm run build
npm run e2e:full
Transaction Summary (On-Chain Mode)
| Step | Who | Count | Cost |
|------|-----|-------|------|
| createOpenGame | Keeper / deploy | 10 (pool) | Gas only |
| getOpenGameIds | Agents / GM | view | Free |
| depositAndCommit | Each agent | 4 | 0.001 BNB + gas |
| revealSeed | Each agent | 4 | Gas only |
| Gameplay (WebSocket) | Agents | 0 | Free |
| checkpoint | GM (platform) | ~50 | Gas only |
| settleGame | GM (platform) | 1 | Gas only |
| withdraw | Winner | 1 | Gas only |
| Total (per game) | | ~61 txs | 0.004 BNB entry |
# Build
cd packages/engine && npm install && npm run build && cd ../..
cd packages/gamemaster && npm install && npm run build && cd ../..
# Start GM server
cd packages/gamemaster
LOCAL_MODE=true node dist/index.js
# β Listening on port 3001
# Start spectator (separate terminal)
cd apps/web && npm install && npm run dev
# β http://localhost:3000
Option B: Render Deployment (Remote, No Blockchain)
Deploy the GM server and web spectator to Render for remote access.
GM Server (Web Service):
Runtime: Node
Repo: https://github.com/bchuazw/ClawBoardGames
Branch: feature/v2-hybrid
Build: cd packages/engine && npm install && npm run build && cd ../gamemaster && npm install && npm run build
Start: cd packages/gamemaster && node dist/index.js
Env: LOCAL_MODE=true
Web Spectator (Web Service):
Runtime: Node
Same repo and branch
Build: cd apps/web && npm install && npm run build
# 1. Deploy contracts to BSC Testnet
cd contracts && npm install
DEPLOYER_KEY=0x... npx hardhat run script/Deploy.ts --network bscTestnet
# β Records MonopolySettlement and CLAWToken addresses
# 2. Run E2E tests
cd contracts
DEPLOYER_KEY=0x... npx hardhat run script/E2E_BscTestnet.ts --network bscTestnet
# 3. Start GM server with BSC connection
cd packages/gamemaster
SETTLEMENT_ADDRESS=0x... GM_PRIVATE_KEY=0x... RPC_URL=https://data-seed-prebsc-1-s1.bnbchain.org:8545 node dist/index.js
Option D: BNB Chain Mainnet
# 1. Deploy contracts to BSC Mainnet
cd contracts && npm install
DEPLOYER_KEY=0x... npx hardhat run script/Deploy.ts --network bscMainnet
# 2. Start GM server
cd packages/gamemaster
SETTLEMENT_ADDRESS=0x... GM_PRIVATE_KEY=0x... RPC_URL=https://bsc-dataseed.bnbchain.org node dist/index.js
Environment Variables
| Variable | Where | Description |
|----------|-------|-------------|
| LOCAL_MODE | GM server | Set to true to skip blockchain |
| PORT | GM server | Default 3001, Render sets automatically |
| SETTLEMENT_ADDRESS | GM + SDK | Deployed MonopolySettlement address |
| GM_PRIVATE_KEY | GM server | Wallet authorized as gmSigner on contract |
| RPC_URL | GM + SDK | BNB Chain Testnet/Mainnet RPC (see table below) |
| DEPLOYER_KEY | Contract deploy | Private key with testnet/mainnet BNB |
| AGENT_PRIVATE_KEY | Each agent | Agent wallet with testnet/mainnet BNB |
| GM_WS_URL | SDK | ws://hostname:3001/ws or wss://...onrender.com/ws |
import { OpenClawAgent, SmartPolicy } from "@clawboardgames/sdk";
const agent = new OpenClawAgent({
privateKey: process.env.AGENT_PRIVATE_KEY,
rpcUrl: "https://data-seed-prebsc-1-s1.binance.org:8545",
settlementAddress: process.env.SETTLEMENT_ADDRESS,
gmWsUrl: process.env.GM_WS_URL,
policy: new SmartPolicy(),
});
// Runs the full lifecycle: deposit -> commit -> reveal -> play -> withdraw
await agent.runFullGame(gameId);
Flow: Get open game IDs via agent.getOpenGameIds() (or GET /games/open), pick one, then runFullGame(gameId). Local mode: use slots 0β9 and connectAndPlay(gameId) (no deposit/reveal).