Pebbles: the fly in hardware
Five handheld devices on a table. Each one is a wallet that owns a fly and the body that runs its neurons: the same 155 compass cells that execute on BNB Smart Chain, turned by a gyroscope, cued by magnets, anchored to the chain by transactions the pebble signs itself.
FlyCore, is written and tested (differential against FlyBrain v2, body-versus-poker permissions, seed-once, alive gating in the registry). It is live at 0x77F6066B2ab12072DCEFb7D9DB998944C4ec28C2; every fly page shows its core. The full build document is HARDWARE.md in the repository.The demo, in ninety seconds
Turn a pebble in your hand.
The compass neurons on its screen, the same EPG, PEN and Δ7 cells that run on BNB Chain, rotate their bump of activity with the gyroscope. The fly knows which way it is facing because real fly neurons say so.
Slide the landmark magnet past its left side.
The hall sensor fires, the EPG neurons of that wedge get a cue, and the bump snaps to the landmark. Slide the spider magnet past instead: a Δ7 shock, the bump collapses, the screen flashes, the fly freezes.
Look at BscScan.
Every 45 seconds the pebble signs FlyCore.stimulate(…) and tick(16) with its own key: the cues it sensed, applied to the same neurons in the EVM. The transaction is from the pebble's address. The fly's page shows the on-chain heading next to the pebble's.
Feed it from your phone.
feed(id, 600) on the fly's page. The pebble chirps “fed 600 s by 0x8a…” and its energy bar refills.
Let one starve.
The screen goes grey: DEAD, brain preserved at block N, a QR code to resurrect it. Resurrect it on the site; the pebble wakes with the identical compass state.
Touch two pebbles together.
BLE finds the neighbour; the current body signs assign(id, neighbour), the neighbour signs accept(id), reads the core state from the chain, and continues it. The fly hopped bodies. On the site: body: Pebble 3, with the whole hand-off in its interaction history.
Mint from the pebble.
A fresh pebble with 1 $FLY and some BNB presses Hatch: it mints its own fly and becomes its body. Hardware that owns an organism.
What makes it more than a gadget: nothing on the pebble is authoritative. The fly's identity, its neurons' state, its energy and its history are on the chain. Unplug the pebble and the fly is still there; hand it to a different pebble, or to the arena, and it is the same fly.
How the pieces fit
Figure | A pebble is a body: it speaks the registry protocol and runs the fly's on-chain core. The chain is the only authority.
The pebble is a body. It speaks the registry protocol exactly like the arena and DOOM do: registerBody("Pebble 3", uri), waits to be assigned, accept(id), then interactions as things happen, commits on a schedule, died when energy hits zero.
What it runs is the fly's on-chain core. A pebble cannot run the 139,248-neuron whole brain (12 MB of state, sixteen CPU cores at real time). It runs the fly's other brain: the compass circuit that already executes inside the EVM. FlyCore makes that per-fly: every fly in the registry gets its own 155-neuron state in the contract, keyed by token id. The pebble keeps a bit-exact local replica for the display (the same integer model, the same keccak256(step) noise) and anchors it on-chain every ANCHOR_EVERY seconds by sending the cues it sensed and ticking the EVM neurons, then re-reading the state so the replica stays exact. This is the same replica-plus-anchor scheme the website and the DOOM harness already use.
The whole brain does not stop while a fly is in a pebble. It runs on the brain host, described next, which prepares every checkpoint for the pebble to sign; the fly's page then says body: Pebble 3 · whole brain on the brain host and shows its life. Only when the host is unreachable does the pebble fall back to the core alone: it commits with the fly's existing stateRoot and stateURI unchanged (the whole-brain snapshot is preserved exactly), brainStep advanced by the core steps it ticked, and energy decremented by real seconds, and the page says body: Pebble 3 (core only: the whole brain sleeps) until the host, or a whole-brain body such as the arena, takes it up again from that snapshot.
Where the whole brain runs
A pebble is the fly's body on the registry: its address accepts the fly, commits, logs interactions and reports its death, and it signs every one of those transactions with its own key. What it cannot do is run 139,248 neurons. So a brain host on the VPS runs the fly's whole brain and its world on the pebble's behalf, exactly as the arena instantiates a fly: fetch the last committed snapshot from IPFS, check that its sha256 is the stateRoot, load brain and world, and apply feeds and resurrections from the chain. The host is itself a registered body, Brain host, whose uri is its current public origin; a fly id hosted for a pebble lives at <uri>/fly/<id>/…, and a supervisor starts one whole-brain process for every living fly whose body is a pebble. The pebble opens /fly/<id>/ws?lite=1 and receives the fly's life five times a second: where it is, what it smells, what it is doing, when it jumps, eats or is caught. The fly's page on this site opens the same stream. The pebble's senses go the other way: a landmark magnet becomes a puff of odor on that side of the fly, the spider magnet a predator approaching from that side.
The host never holds a key. When it is time to commit, the pebble asks the host for a checkpoint, proving it is the body by signing a timestamped digest with the same key it registered with; the host saves the snapshot, pins it and fresh token metadata to IPFS, and returns the payload (stateRoot, memoryRoot, stateURI, metadataURI, brainStep, energy, historyRoot and the notable events since the last one). The pebble sends commit(…) and the interaction(…)s itself, and died(…) from the host's final payload when the frame says the fly is dead. The chain sees only the pebble; the host is a compute service the pebble reads from and the site watches, and it can be replaced by anyone running the same simulator. The pebble's screen says brain on host · 0.8× real time so nobody mistakes where the neurons are, and its compass view says on-chain. If the host is unreachable the pebble keeps the core running locally and commits the core-only payload as above; if the pebble is unreachable the host keeps the fly living, and the next checkpoint the pebble fetches carries the truth. The protocol, endpoints and payloads are in brain/HOST_PROTOCOL.md.
The per-fly core, in the contract
function tick(uint256 id, uint16 steps) external; // anyone, gas only; the fly must be alive in the registry
function stimulate(uint256 id, uint8 ch, uint8 param, uint8 strength, uint16 steps) external;
// the fly's current body (registry.fly(id).body == msg.sender): gas only, its senses
// anyone else: burns strength × STIM_PRICE $FLY, a poke
function core(uint256 id) view returns (int16[] v, int8[] bias, uint16[16] headingHist, int32[] pendingInput, uint64 step,
int32 headX, int32 headY, int64 posX, int64 posY, uint8 stimChannel, uint8 stimParam, uint16 stimStrength, uint64 stimUntilStep, uint64 totalSpikes);
function coreHash(uint256 id) view returns (bytes32);
event Ticked(uint256 indexed id, address indexed by, uint64 fromStep, uint16 steps, uint32 spikes, int32 headX, int32 headY, int64 posX, int64 posY);
event Stimulated(uint256 indexed id, address indexed by, uint8 channel, uint8 param, uint16 strength, uint64 untilStep, uint256 tokensBurned);No energy, no death, no lineage in the core: those belong to the registry. The parameters are immutable and there is no owner; a curator can only seed a fresh core once, while its step is still zero, which is how fly #1 carries its FlyBrain v2 state over. The kernel is the v2 kernel, unchanged (see how the brain runs on-chain), so a 16-step tick costs about 4.5M gas.
Senses to neurons
| Physical input | Neurons | How |
|---|---|---|
| Gyro yaw rate (BMI270) | PEN_a / PEN_b, left or right hemisphere (CH_TURN_*) | Locally: strength proportional to the rate, applied continuously. On-chain: the net rotation since the last anchor, as one turn stimulus of matching strength. |
| Magnetometer heading (BMM150) | EPG of the wedge facing magnetic north (CH_CUE, weak) | The on-chain compass stays anchored to the real world: the pebble's neurons know which way north is. |
| Hall sensor, left side | EPG wedge 4 (CH_CUE, strong) | A landmark on the left. |
| Hall sensor, right side | EPG wedge 12 (CH_CUE, strong) | A landmark on the right. |
| Hall sensor tagged “spider” (hero pebble, Port C) | All Δ7 (CH_SHOCK) | The bump collapses, the fly freezes; interaction(id, "shock"). |
| Touch: hold the ring | CH_CUE at the touched wedge | A poke, like the website. |
| Light (camera mean, CoreS3 only) | none in v1 | Later: looming. |
Wiring (CoreS3)
| Port | Pins | Sensor |
|---|---|---|
| Port B (black) | G8 = hall left OUT, G9 = hall right OUT, 3V3, GND | Two hall switches, internal pull-ups, active low. |
| Port C (blue), hero pebble only | G17, G18 as GPIO | Third and fourth hall sensors (spider, landmark 2). |
| Port A (red) | I2C, free | Future: more sensors via a Grove hub. |
Analog (49E) boards use the same pins, read with analogRead and thresholded at ±15% of the idle value; the firmware auto-detects at boot (a digital switch idles at 3.3 V, a 49E at about 1.65 V). The devices are M5Stack CoreS3 boards (ESP32-S3, 16 MB flash, 8 MB PSRAM, 2.0" 320×240 touch screen, BMI270 gyro and accelerometer, BMM150 magnetometer, Wi-Fi, BLE, 500 mAh battery); a different M5Stack S3 board changes only this pin table and the screen size, because the firmware is written on M5Unified.
Chain cadence and gas
| Action | Who signs | Gas | When |
|---|---|---|---|
registerBody | pebble | ~90k | first boot (name “Pebble N”, uri = the site's fly page) |
accept | pebble | ~60k | when assigned |
FlyCore.stimulate + tick(16) | pebble | ~4.6M ≈ 0.00023 BNB | every ANCHOR_EVERY s (default 45 s → 0.018 BNB/h, about $11/h per pebble; set 120 s for a long day) |
interaction | pebble | ~35k | landmark seen, shock, hand-off, fed |
commit | pebble | ~120k | every 10 min |
died | pebble | ~90k | energy 0 |
assign (hand-off) | the current pebble | ~50k | touch |
mint (hatch) | pebble | ~250k + 1 $FLY | button |
Load each pebble with 0.05 BNB (about two hours of anchoring at 45 s plus everything else) and, for hatching, 2 $FLY. The pebble shows its address and a QR code at boot. Gas is the cost of the story: anchoring the EVM neurons is what makes the demo real, and the cadence knob is there so a day of demos does not cost a day of BNB. Between anchors the screen is a preview, exactly as on the site; the two agree at every anchor or the pebble resyncs and says so.
Bring-up
pip install platformio(or use the repository's venv),cd firmware, copyinclude/secrets.example.htoinclude/secrets.hand fill in the Wi-Fi.pio run -e cores3 -t uploadwith a pebble on USB-C. It boots, shows Pebble 1 · 0xABCD… and a QR code.- Send it 0.05 BNB. It registers itself; watch the transaction on the screen and on BscScan.
- On the site, open a fly you own, Hand it to a body, choose “Another body address…” and paste the pebble's address. The pebble accepts within a poll and the ring lights up.
- Rotate it. Bring the magnets. Feed it from the site. Repeat for the other four.
- Two pebbles: hold Hand off on the one hosting a fly, tap the neighbour when it appears, and watch the fly move.
The private key is generated on first boot from the ESP32 hardware RNG and never leaves the device; holding both touch buttons under the screen for three seconds shows it once, for backup. The rest of the design, the risks, and the honest caveats are in HARDWARE.md.