How it works. All of it.
Overview
goBROKR is a location game where the loot is real. Fragments of bStocks (Binance-issued tokenized US stocks on BNB Smart Chain) are attached to physical places: the store of the company behind the token. You open the app, walk to the place, tap, and the fragment is transferred to your wallet.
Three parts make it work: a spawn table (where and how much), a claim server that checks you are really there and signs a voucher, and a vault contract that holds the tokens and only releases them against a valid voucher. Nothing is minted. Every fragment on the map was bought by the treasury and deposited before it spawned.
Drops
A drop is a spawn point: a token, an amount, a rarity, coordinates and a place name. Today 470 drops are seeded across 36 cities, sourced from OpenStreetMap brand data and thinned so a city is not a wall of pins. Each token has one drop rule that applies everywhere:
| Token | Where | Fragment | Rarity |
|---|---|---|---|
| GameStop / EB Games / Micromania stores | 0.15 | common | |
| AMC and Odeon cinemas | 1 | common | |
| Whole Foods Market, Amazon Fresh and Amazon Go | 0.015 | common | |
| Freshippo (盒马) stores | 0.025 | common | |
| Apple Stores | 0.011 | rare | |
| Google offices | 0.012 | rare | |
| Microsoft offices and stores | 0.007 | rare | |
| Meta offices | 0.005 | rare | |
| Tesla stores and showrooms | 0.013 | epic | |
| One announced door, anywhere on the map | 1 full share | legendary |
Drops refill every day at midnight in the drop's own time zone. A drop can be caught at most 24 times per day and rests 10 minutes after each catch; a wallet can catch at most 6 drops per day (counted in UTC), one every 2 hours, and the same drop once per day.
Claim flow
- You connect a wallet and enable location. The app keeps a rolling log of GPS fixes.
- When you are within 40 m plus half your reported error (capped at 73 m) with accuracy under 65 m, the catch button unlocks.
- The app sends your wallet, the drop id, the last 4+ fixes, your device time zone and a motion reading to
POST /api/claim. - The server re-checks distance and accuracy, reads the GPS log behind the fix, compares it with your previous accepted fix (no teleporting, no city hopping inside an hour), applies the caps, then builds a voucher.
- The server signs the voucher with the claim key (EIP-712) and returns it. The voucher expires in 15 minutes.
- Your wallet calls
claim(voucher, signature)on the vault. The contract verifies the signature, burns the nonce and transfers the fragment to you.
struct Voucher {
address to; // your wallet
address token; // e.g. AAPLon 0x431a3bee82e2ca41e49895cbece5bb0f76a89b7a
uint256 amount; // 0.011 AAPL = 11000000000000000 (18 decimals)
bytes32 spawnId; // keccak256("shanghai-aapl-0")
uint256 nonce; // unique, burned on use
uint256 deadline; // unix seconds
}The chain never trusts the app. It trusts one key, and that key only signs after the physical checks. If the key leaks, the owner rotates it with setSigner and pauses the vault.
Vault contract
BrokrVault.sol is deliberately small. It holds ERC-20 stock tokens and exposes one user function:
function claim(Voucher calldata v, bytes calldata sig) external // reverts: Paused, Expired, NonceUsed, BadSignature, TransferFailed // effects: marks nonce used, transfers v.amount of v.token to v.to, emits Claimed
Owner functions: setSigner, setPaused, withdraw(token, to, amount) for any ERC-20, withdrawNative(to, amount) for BNB, and transferOwnership. Anyone can relay a voucher, but tokens always go to the address inside it. Signatures use EIP-712 with domain goBROKR / 1 / chainId 56 / vault address, so a voucher for one vault is worthless on another. Source lives in contracts/, built and tested with Foundry.
Treasury & $BROKR
$BROKR is a tax token launched on Flap. Every buy and sell pays a tax defined in the token contract, and that tax is routed to the treasury wallet. The treasury swaps BNB for tokenized stocks on PancakeSwap and deposits them into the vault. There are no emissions, no rewards pool and no points: what the vault holds is what the map can pay, and both are readable on BscScan.
The proof page reads balanceOf(vault) for every token and the treasury's BNB balance once every 30 seconds and serves the snapshot from cache, so the page never waits on an RPC.
Anti-cheat
GPS can be faked and wallets are free, so per-wallet limits alone are worthless. Every check below is keyed on something a fresh wallet does not reset:
- Radius and accuracy. 40–73 m, scaled to the error your fix reports, and accuracy under 65 m. Indoor and coarse IP fixes are rejected.
- Proof of presence. At least 4 readings over 20 seconds. A real chipset wobbles and its accuracy drifts; a mock provider returns the same numbers forever, and that is exactly what we look for.
- Travel. A new catch has to be reachable from the last one: walking speed under 300 km, a plane plus 90 minutes of airport beyond it. Two different cities inside one hour is refused whatever the distance.
- Cross-checks GPS cannot fake. The country of your connection has to match the door's country; a device clock more than 3 hours off the door's time zone is refused; an accelerometer reading exactly zero is refused.
- Mainland China. Phones there may report GCJ-02 coordinates, 300–600 m off. Doors stay in WGS-84 and the walker's fix is de-shifted once, as it arrives; a catch is accepted if either frame puts you at the door.
- Cooldowns and caps. 2 h between catches by the same wallet, 10 minutes before the same drop wakes up, 6 catches per wallet per day, 24 per drop per day.
- Nonces and deadlines. A voucher is single-use and expires in 15 minutes.
A rejected claim consumes nothing, so nobody can burn someone else's allowance.
Tokens
bStocks are BEP-20 tokens issued by BTech Holdings (a Binance affiliate), one contract per underlying, 18 decimals, backed 1:1 by the real share held at a regulated custodian with a daily Proof of Collateral. Convertible 1:1 to the stock on Binance; on-chain they are ordinary ERC-20s and trade on PancakeSwap and Binance Wallet. 80 tokens are wired into the app, regenerated from Binance's public RWA registry by scripts/fetch-rwa.mjs, logos self-hosted.
Chain id 56, explorer bscscan.com. The app adds the network to your wallet automatically.
API
GET /api/spawns?lat=&lng=&limit= drops sorted by distance (all if no position)
POST /api/claim { address, spawnId, fixes[], tz, motion }
→ { ok, mode, voucher, signature, contract, chainId }
GET /api/claims public feed, addresses shortened
GET /api/claims?address=0x… one wallet's catches
GET /api/vault balances, drops payable, limits in forceErrors come back as { ok: false, error: "human sentence", code } with a 4xx status. The claim endpoint is the only one that signs anything.
Demo vs live
The claim server runs in demo mode until two env vars exist: CLAIM_SIGNER_KEY and NEXT_PUBLIC_VAULT_ADDRESS. In demo mode every check runs for real and the catch is logged under your wallet, but no voucher is signed and nothing moves on-chain. In live mode the voucher is signed and your wallet sends the claim transaction. Never put the signer key in the browser; it only exists in the server route that signs vouchers.