documentation

How it works. All of it.

Overview

Touch Grass is a location game where the loot is real. Fragments of Robinhood Chain stock tokens are attached to physical places: the store or office of the company behind the token. You open the app, walk to the place, tap, and the fragment is transferred to your wallet on Robinhood Chain.

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 and deposited before it spawned.

Drops

A drop is a spawn point: a token, an amount, a rarity, coordinates and a place name. Today 88 drops are seeded across 13 cities. Each token has one drop rule that applies everywhere:

TokenFragmentRarity
AMZN0.005common
UPS0.01common
GME0.05common
LULU0.008common
AAPL0.01rare
GOOGL0.004rare
META0.004rare
NFLX0.003rare
MSFT0.004rare
TSLA0.003epic
COST0.002epic
NU0.05epic
NVDA1 full sharelegendary

Drops refill every day at midnight UTC. A drop can be caught at most 40 times per day, a wallet can catch at most 6 drops per day and the same drop once per day. Legendary drops (a full NVDA share at Nvidia HQ) are monthly events announced 24 hours ahead and require scanning a code on site.

Claim flow

Six steps, four of them invisible:

  1. You connect a wallet and enable location. The app watches your GPS position.
  2. When you are within 40 m of a drop with accuracy under 65 m, the catch button unlocks.
  3. The app sends your wallet address, the drop id and your fix to POST /api/claim.
  4. The server re-checks distance and accuracy, compares with your previous accepted fix (no teleporting), applies the daily caps, then builds a voucher: to, token, amount, spawnId, nonce, deadline (15 minutes).
  5. The server signs the voucher with the claim key (EIP-712) and returns it.
  6. Your wallet calls claim(voucher, signature) on the vault. The contract verifies the signature, burns the nonce and transfers the fragment to you.
Voucher {
  address to;       // your wallet
  address token;    // e.g. AAPL 0xaF3D…93f9
  uint256 amount;   // 0.01 AAPL = 10000000000000000 (18 decimals)
  bytes32 spawnId;  // keccak256("paris-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

TouchGrassVault.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 if: paused, past deadline, nonce already used, signer mismatch
  // effects: marks nonce used, transfers v.amount of v.token to v.to, emits Claimed

Owner functions: setSigner, setPaused, withdraw, transferOwnership. Anyone can relay a voucher, but tokens always go to the address inside it. Signatures use EIP-712 with domain TouchGrass / 1 / chainId 4663 / vault address, so a voucher for one vault is worthless on another. Source lives in contracts/, compiled with solc-js by scripts/compile-vault.mjs, deployed by scripts/deploy-vault.mjs.

Proof of reserves

The vault address is public and every balance below is read from Robinhood Chain when you load this page. There is no off-chain ledger: if a token shows zero drops here, that drop cannot be caught, and the app says so before you walk.

the vault, live from the chain
Reading…
drops payable right now

Every number above is a balanceOf call on Robinhood Chain, refreshed every minute. Click any token to see the same balance on the explorer. Nothing is minted: a drop only spawns if the vault can pay it.

Anti-cheat

GPS can be faked. We stack cheap checks so faking is annoying and rarely worth it:

  • Radius and accuracy. 40 m radius, accuracy under 65 m. Indoor fixes and coarse IP fixes are rejected.
  • Speed. Your last accepted fix is stored. Moving faster than 9 m/s between two claims is rejected.
  • Caps. Per wallet per day, per drop per day, one catch per drop per wallet per day, and the fragment sizes are small.
  • Nonces and deadlines. A voucher is single-use and expires in 15 minutes. Replays are impossible.
  • Device attestation (roadmap). The native app will attach Play Integrity / App Attest tokens to each claim, which kills emulators and most spoofing apps.
  • On-site codes. Legendary drops also require scanning a rotating code physically present at the location.

The economics do the rest: a cheater burns effort for fractions of a share, while a sponsored drop is priced per verified visit and can be refunded when a claim is later flagged.

Tokens

Robinhood Chain stock tokens are ERC-20s issued by Robinhood, one contract per underlying, 18 decimals, tracking the share price through a per-asset price feed. They are available in 120+ countries and not to US persons. 42 tokens are wired into the app today, each with its on-chain icon and contract address in src/lib/tokens.json, regenerated from Robinhood's public registry by scripts/fetch-tokens.py.

Chain id 4663, RPC https://rpc.mainnet.chain.robinhood.com, explorer robinhoodchain.blockscout.com. The app adds the network to your wallet automatically.

Brands

A brand sponsors drops at its own doors. It funds the vault with its stock token (or pays us to buy it), we schedule spawns, and it pays per verified visit: a claim that passed every check above. Not per impression, not per click. The dashboard for this is the next thing we build; today it goes through the early access form with the brand option.

API

GET  /api/spawns?lat=&lng=&limit=       drops sorted by distance (all if no position)
POST /api/claim  { address, spawnId, lat, lng, accuracy }
                                        → { ok, mode, voucher, signature, contract, chainId }
GET  /api/claims                         public feed, addresses shortened
GET  /api/claims?address=0x…             one wallet's catches
POST /api/early-access { email, kind, city?, company? }

Errors come back as { ok: false, error: "human sentence" } 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 (the signing key) and NEXT_PUBLIC_VAULT_ADDRESS (a deployed, funded vault). 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.

State (caps, last fix, catches, signups) lives in Redis when UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN or Vercel KV vars are set, and in memory otherwise.

Keeper

A Vercel cron job calls /api/cron/fund every hour. It reads how many drops the vault can still pay for each token, and for every token under the target (10 drops by default) it buys more with the treasury's ETH through the TouchGrassSwapper contract: ETH to USDG to the stock token in one transaction, output delivered straight into the vault. Each buy is simulated first, capped per run, and logged. The treasury key is the on/off switch: no key, no purchases.

Tokens without an on-chain pool yet (NFLX, LULU, COST, UPS, NU) are skipped and must be deposited by hand. The last keeper runs are visible in /api/vault.

Run it yourself

npm install && npm run dev            # site on :3000, demo mode

# go live
node scripts/compile-vault.mjs        # → src/lib/vault.artifact.json
DEPLOYER_KEY=0x… CLAIM_SIGNER_KEY=0x… node scripts/deploy-vault.mjs
# fund the vault: send stock tokens to the printed address
# set NEXT_PUBLIC_VAULT_ADDRESS + CLAIM_SIGNER_KEY (+ Upstash vars) in Vercel, redeploy

Never put the signer key in the browser. It only exists in the server route that signs vouchers.