Flappy Robin docs

What it is

Flappy Robin is a one-button flying game. Its engine is a WebAssembly module stored as contract bytecode on Robinhood Chain (chain 4663). This site only ships the host: it reads the engine from the chain, checks every byte, then runs it.

The candles you fly through are real blocks, read live while you play. Every hour, the five best runs split a prize pool funded by the token's creator fees.

How the money moves

How the money moves, step by step The token launches on Pons. Every trade pays a 3% fee, and the vault splits what it receives in half: half to the treasury, half to this round's prize pool. You hold at least the minimum and play ranked runs. The round closes an hour later and you must still be holding then. After a short review, the top five runs are paid 50, 25, 10, 9 and 6% of the pool. The next round is already running. The next round is already running Launch on Pons Trade 3% fee Vault 50% prizes 50% treasury Play hold the minimum Round closes still holding Top 5 paid after a review How the money moves, step by step The token launches on Pons. Every trade pays a 3% fee, and the vault splits what it receives in half: half to the treasury, half to this round's prize pool. You hold at least the minimum and play ranked runs. The round closes an hour later and you must still be holding then. After a short review, the top five runs are paid 50, 25, 10, 9 and 6% of the pool. The next round is already running. Launch on Pons Trade 3% fee Vault 50% prizes, 50% treasury Play hold the minimum Round closes still holding Top 5 paid after a review The next round is already running

Rewards

Preview: the token does not exist yet. Until it launches, nobody plays and no round pays.

FeesThe token's creator fees go to the RewardVault contract
Split50% to the treasury, 50% to the hourly prize pool
HoursUTC hours. The pool pays the five best runs 50, 25, 10, 9 and 6%
Empty ranksA rank without a run rolls its share into the next hour
Same scoreThe run that reached it first takes the rank
Hour endA run still going at :00 ends there. It counts in the hour its ticket was bought in, so tickets stop about 10 seconds before :00
EligibleA linked wallet holding at least the minimum when the run starts and at payout. The owner can change the minimum; a change takes effect at the next round, never during one
Your scoreYour best run of the hour
  1. The hour closes at :00.
  2. The top five runs go through bot detection, and their replays are published.
  3. Payouts go out 5 minutes later. Each links its transaction and each winning run its replay.
  4. A flagged run's share is held in the vault while the other ranks are paid on time. The owner wallet approves it (paid) or rejects it (rolled into the next hour). Unresolved after 24 hours, it rolls.
Owner0x1D589480f7649D82d4C8430d34f918E672f5C373. Approves or rejects held shares, rotates the oracle key, and can move the prize pool to the treasury in an emergency. Its key stays offline, never on a server.
TreasuryThe same owner wallet. An emergency withdrawal therefore sends the prize pool to the owner.
OracleA separate key that can only submit each hour's results. The contract computes the amounts from the fixed shares.
Emergency brakeThe vault can be paused only against theft or a chain incident, never as part of a round. While it is paused the settle and hold clocks stop, so nothing owed to a winner can expire, and the site says so in one line.

Fair play

What is checked:

  • Replays. Every run is played again on the server from its input log, with the engine bytes read from Robinhood Chain. The score must match.
  • Unknown in advance. Each run mixes a server seed with hashes of blocks produced after it starts, so a course cannot be prepared ahead.
  • Live inputs. Flaps are sent while you play and checked for human pacing.
  • Limits. A bot check when a run starts, and rate limits per wallet.
  • Review. The top five of every hour are reviewed before payout, and their replays are public.

What cannot be fully prevented, even then:

  • A bot that plays like a person in a real browser can still compete. Review and public replays make it visible, not impossible.
  • Several wallets, each holding the minimum, count as several players.
  • The scoring server is trusted: a replay proves a run is valid, not who played it. The keeper re-checks every winner and a watcher can veto an hour for 5 minutes.
  • A run tolerates about 150 ms of network lateness, and a slow or unsteady connection can lose a run to lag.
  • While you play, the page knows at most 3 upcoming candles. A modified page can let you decide each flap about 0.15 s after it shows the moment.
  • If the payout keeper is down for more than 6 hours, anyone can execute a submitted hour.
  • Replays are proven identical in Chromium-based browsers; Firefox and Safari are untested.

What lives on chain

One ROM contract and its chunks. The ROM is born sealed in a single transaction: its constructor stores every chunk, computes the Merkle root and body hash on chain, and it has no owner. Nothing can be added or replaced later.

ROM contract
Root
Body hash
Raw hash
Chunks
Bytes on chain
Bundle bytes
  • Chunks are data contracts whose code is 0x00 followed by up to 24,575 bytes. The ROM contract creates them itself.
  • The body is a gzipped CROM v1 bundle with two files: flappy-robin.wasm and README.txt.
  • The read surface matches ChainRom, so ChainRom-style readers can load it too.

How your browser checks it

Every step must pass before anything runs. The expected root, body hash and raw hash are pinned in this site's code, so an RPC can fail to deliver the game but cannot swap it.

  1. eth_chainId must be 4663.
  2. The header comes in one batch. sealed_() must be true, and root(), bodyHash() and rawHash() must equal the pins.
  3. pointers() lists the chunks, then one eth_getCode batch reads them. Each must start with 0x00.
  4. The Merkle root is rebuilt from the chunks and must equal the pinned root.
  5. The chunks are joined. Their keccak256 must equal the body hash, their length bodyBytes().
  6. The body is inflated. Its keccak256 must equal the raw hash, its length rawBytes().
  7. The first .wasm in the bundle is compiled. It may import only present, set_palette, poll_event and now_ms.

Until the ROM contract is published, the game page runs a preview build served by this site. It passes the same checks from step 4 on, against pinned values in this site's code, and the chip under the game says Preview build.

No JavaScript is ever loaded from chain. The engine runs in the WebAssembly sandbox and talks to the page only through those four imports and its own exports.

Merkle rule: leaf = keccak256(chunk), parent = keccak256(left ‖ right), an odd last node moves up unpaired. With one chunk, the root equals the body hash.

"Check a chunk on chain", behind the "Loaded from Robinhood Chain" button under the game, asks the contract's own verify(i, proof). That answer still travels through your RPC, so the pins above are what actually protect you.

Check it yourself

With Foundry's cast:

ROM=0x…
RPC=https://rpc.mainnet.chain.robinhood.com

cast call $ROM "sealed_()(bool)" --rpc-url $RPC
cast call $ROM "root()(bytes32)" --rpc-url $RPC
cast call $ROM "pointers()(address[])" --rpc-url $RPC

With one chunk, the root is the hash of the chunk's code minus its first byte:

P=$(cast call $ROM "chunks(uint256)(address)" 0 --rpc-url $RPC)
cast keccak $(cast code $P --rpc-url $RPC | sed 's/^0x00/0x/')

With plain curl (0xebf0c717 is root()):

curl -s $RPC -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"'$ROM'","data":"0xebf0c717"},"latest"]}'

The full check, from a checkout of the source (Node 18 or newer):

node tools/verify-rom.mjs --rpc $RPC --rom $ROM

Live data

SourceWhatHow often
This site's /api/stateLatest block: number, hash, gas used, transactions, base fee — read from Robinhood Chain once every 3 s by the server, whatever the number of players5 s
Server, from Robinhood ChainOne block from 3,000 blocks earlier, to measure block timeOnce
Server, from Robinhood ChainA few blocks around 00:00 UTC, to find the first block of the dayOnce a day
Server, from api.coinbase.comETH spot price, only to show amounts in US dollars60 s
  • Each candle takes the newest unused block. Its hash sets the gap, its gas used against the previous candle sets the colour. Hitboxes depend only on the gap.
  • With no block newer than 3 s, the next candle is grey, labelled ----, and placed by a local random source.
  • The sky follows New York Stock Exchange hours, read from block timestamps.
  • Transactions per second count every transaction but the first of each block, which is the chain's own system transaction.
  • The ticker under the game shows Robinhood Chain and this game only: pool, leaders, payouts, block, TPS, gas, blocks today, the next round block number and the day of mainnet, counted from July 1, 2026.
  • Dollar amounts are ETH at the Coinbase spot price. Without a price, amounts show in ETH only.
  • The page never calls a chain node itself: everything above arrives through this site's own /api/state. Requests stop while the tab is hidden and are abandoned after 10 seconds. This site runs no analytics. Signing in sets one cookie, which only keeps you signed in; disconnecting your wallet clears it.

Contract reference

FlappyRom, Solidity 0.8.30. Every function is read-only.

SelectorFunctionReturns
06fdde03name()string, "Flappy Robin"
26d111f5note()string
8da5cb5bowner()address, always zero
ebf0c717root()bytes32, computed in the constructor
ce57b4a0bodyHash()bytes32, keccak256 of all chunks, computed
e2332df3rawHash()bytes32, keccak256 of the bundle, set by the publisher
1ac27abcbodyBytes()uint32, computed
1cb56a14rawBytes()uint32, set by the publisher
bf5390b9sealed_()bool, always true
f91f0937chunkCount()uint256
5a2bc22dchunks(uint256 i)address of chunk i
ed2e5a97read(uint256 i)bytes, chunk i without its 0x00
a79a2a28verify(uint256 i, bytes32[] proof)bool, chunk i is under the root
71267571pointers()address[], every chunk
8c72c54epublisher()address that deployed the ROM

Events: Wrote(uint256 indexed index, address pointer, uint256 length) once per chunk, then Sealed(bytes32 root, uint256 chunkCount, uint32 rawBytes) once.

Engine interface

The engine imports exactly ChainRom's four screen functions from module env, so it also boots on ChainRom's reader, keyboard only and silent.

ImportSignatureMeaning
present(ptr, w, h, format, stride) → i32Show a 144×256 indexed frame
set_palette(ptr, count) → i32RGB triples, 8-bit
poll_event() → i32(type << 24) | code, 0 when empty
now_ms() → f64Milliseconds, monotonic

Event types: 1 key down, 2 key up, 3 press (pointer or touch), 4 pause. Key codes: Arrow Up 1, Arrow Down 2, Arrow Left 3, Arrow Right 4, Control 5, Space 6, Enter 7, Escape 8, Alt 9, Shift 10, Tab 11. This page also sends W as 1 and P as 8.

ExportSignatureMeaning
memory, frameframe()Once per animation frame. Fixed 120 Hz steps inside
feed_hash_ptr, feed_block(number, gas_used, tx_count, timestamp)Push a block; its 32-byte hash goes to the pointer first
msg_ptr, push_msg(len, tone)Ticker message, up to 48 ASCII bytes
set_seed(hi, lo)Seed for the offline candle generator
set_best, set_flags(n), (bits)Restore best score; bit 0 reduced motion, bit 1 muted
score, best, state() → i32State 0 ready, 1 playing, 2 paused, 3 dying, 4 game over
ui_take() → i32Bits since last call: start, scored, died, new best, paused, resumed, ready
sfx_take, sfx_rate, sfx_count() → i32Sound bits since last call, sample rate 22,050, number of sounds
sfx_ptr, sfx_len(id) → i32Sounds synthesized by the engine: 16-bit mono PCM, length in samples

Cost

Publish transaction
Gas used
Gas price
Fee
Published

Contract code costs L2 gas only on Robinhood Chain today, about 218 gas per stored byte.

Token

Only trust the address shown on this site.