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
Rewards
Preview: the token does not exist yet. Until it launches, nobody plays and no round pays.
| Fees | The token's creator fees go to the RewardVault contract |
|---|---|
| Split | 50% to the treasury, 50% to the hourly prize pool |
| Hours | UTC hours. The pool pays the five best runs 50, 25, 10, 9 and 6% |
| Empty ranks | A rank without a run rolls its share into the next hour |
| Same score | The run that reached it first takes the rank |
| Hour end | A 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 |
| Eligible | A 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 score | Your best run of the hour |
- The hour closes at :00.
- The top five runs go through bot detection, and their replays are published.
- Payouts go out 5 minutes later. Each links its transaction and each winning run its replay.
- 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.
| Owner | 0x1D589480f7649D82d4C8430d34f918E672f5C373. 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. |
|---|---|
| Treasury | The same owner wallet. An emergency withdrawal therefore sends the prize pool to the owner. |
| Oracle | A separate key that can only submit each hour's results. The contract computes the amounts from the fixed shares. |
| Emergency brake | The 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.
Not published yet
| ROM contract | |
|---|---|
| Root | |
| Body hash | |
| Raw hash | |
| Chunks | |
| Bytes on chain | |
| Bundle bytes |
- Chunks are data contracts whose code is
0x00followed 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.wasmandREADME.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.
eth_chainIdmust be 4663.- The header comes in one batch.
sealed_()must be true, androot(),bodyHash()andrawHash()must equal the pins. pointers()lists the chunks, then oneeth_getCodebatch reads them. Each must start with0x00.- The Merkle root is rebuilt from the chunks and must equal the pinned root.
- The chunks are joined. Their keccak256 must equal the body hash, their length
bodyBytes(). - The body is inflated. Its keccak256 must equal the raw hash, its length
rawBytes(). - The first
.wasmin the bundle is compiled. It may import onlypresent,set_palette,poll_eventandnow_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
| Source | What | How often |
|---|---|---|
This site's /api/state | Latest block: number, hash, gas used, transactions, base fee — read from Robinhood Chain once every 3 s by the server, whatever the number of players | 5 s |
| Server, from Robinhood Chain | One block from 3,000 blocks earlier, to measure block time | Once |
| Server, from Robinhood Chain | A few blocks around 00:00 UTC, to find the first block of the day | Once a day |
Server, from api.coinbase.com | ETH spot price, only to show amounts in US dollars | 60 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.
| Selector | Function | Returns |
|---|---|---|
06fdde03 | name() | string, "Flappy Robin" |
26d111f5 | note() | string |
8da5cb5b | owner() | address, always zero |
ebf0c717 | root() | bytes32, computed in the constructor |
ce57b4a0 | bodyHash() | bytes32, keccak256 of all chunks, computed |
e2332df3 | rawHash() | bytes32, keccak256 of the bundle, set by the publisher |
1ac27abc | bodyBytes() | uint32, computed |
1cb56a14 | rawBytes() | uint32, set by the publisher |
bf5390b9 | sealed_() | bool, always true |
f91f0937 | chunkCount() | uint256 |
5a2bc22d | chunks(uint256 i) | address of chunk i |
ed2e5a97 | read(uint256 i) | bytes, chunk i without its 0x00 |
a79a2a28 | verify(uint256 i, bytes32[] proof) | bool, chunk i is under the root |
71267571 | pointers() | address[], every chunk |
8c72c54e | publisher() | 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.
| Import | Signature | Meaning |
|---|---|---|
present | (ptr, w, h, format, stride) → i32 | Show a 144×256 indexed frame |
set_palette | (ptr, count) → i32 | RGB triples, 8-bit |
poll_event | () → i32 | (type << 24) | code, 0 when empty |
now_ms | () → f64 | Milliseconds, 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.
| Export | Signature | Meaning |
|---|---|---|
memory, frame | frame() | 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 | () → i32 | State 0 ready, 1 playing, 2 paused, 3 dying, 4 game over |
ui_take | () → i32 | Bits since last call: start, scored, died, new best, paused, resumed, ready |
sfx_take, sfx_rate, sfx_count | () → i32 | Sound bits since last call, sample rate 22,050, number of sounds |
sfx_ptr, sfx_len | (id) → i32 | Sounds synthesized by the engine: 16-bit mono PCM, length in samples |
Cost
Not published yet
| 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.