Collaborative, verifiable game progress through randomness mining.
Seedrun is a protocol for discovering, recording, and monetising emergent progress in deterministic video games driven entirely by random input. Where Twitch Plays Pokémon crowdsources human inputs, Seedrun crowdsources seeds — compact values that deterministically expand into a sequence of controller inputs. Participants ("miners") search for seeds whose random inputs happen to drive a game forward, and submit the best ones to an Ethereum smart contract. A single canonical run is assembled segment-by-segment from these submissions, forming a verifiable, append-only chain of game state.
The blockchain serves only as an ordered, censorship-resistant log. All validation — replaying the emulator, grading frames, and scoring submissions — is performed off-chain by the gateway software that any party can run to independently verify the canonical run. A minable coin economy — with an optional ERC-20 wrapper — lets participants trade the value they create, and doubles as an escalating stake that prices out submission spam.
Deterministic emulators have a useful property: given the same ROM, the same initial state, and the same sequence of inputs, they always produce the same sequence of frames. This makes any run trivially reproducible and therefore trustlessly verifiable — anyone can replay the inputs and confirm the result without trusting the submitter.
"RNG Plays Pokémon", "Fish Plays Pokémon" and "Pi Plays Pokémon" demonstrated that purely random inputs can, with enough attempts, stumble into real in-game progress. Seedrun turns that observation into a coordination game:
Seedrun provides the rules, the scoring, and the economic rails to make this a sustained, collaborative effort rather than a one-off curiosity.
Seedrun has two layers:
| Layer | Responsibility |
|---|---|
| On-chain (Ethereum) | An ordered, immutable log of intents: run creation, seed submissions, and coin transfers. Performs no game logic. |
| Off-chain (the gateway) | The user's read/write portal to the network. Each participant runs their own gateway: it reads the chain (consuming contract events, replaying the emulator, scoring submissions, maintaining the canonical run, deriving coin balances), writes to the chain on the user's behalf (submitting seeds and other transactions via the user's local wallet), and serves the web UI. |
The gateway is deliberately not a shared backend API. It is software each user runs themselves — closer in spirit to an Ethereum RPC gateway or a self-hosted node than to a hosted indexing service. Reading and deriving the canonical run is one half of what it does (this read/derive component is referred to below as the indexer); the other half is submitting the user's own transactions with a local key.
The guiding principle is that the chain orders, the gateway validates. The contracts deliberately accept unauthenticated, unconstrained input and simply emit events. Invalid or forged submissions remain on-chain forever but are ignored by every honest gateway, because the rules that decide validity are deterministic and reproducible by anyone.
This split keeps gas costs minimal (events are cheap; emulation is not something you would ever want to do on-chain) while preserving the trustlessness that matters: the canonical run is a pure function of the on-chain event log and the public rules.
A run is a single continuous playthrough of one game under one fixed set of
rules. A run is announced on-chain via createRun(settings), where settings
is a CBOR-encoded RunSettings blob that itself includes the ROM hash. The
CreateRun event carries only this single blob — there is no separate ROM
argument.
A run is divided into segments of a fixed number of frames
(frames_per_segment). Each segment is contributed by one accepted seed
submission. Segments are chained: each builds on the resulting emulator state
of the one before it, so the run grows monotonically as new segments are
accepted.
RunSettings is serialised to a compact CBOR blob and carried on-chain as
the sole argument to createRun. It bundles the ROM hash with every parameter
the indexer needs to reproduce the run:
| Field | Meaning |
|---|---|
rom | keccak256 hash (32 bytes) identifying the exact game ROM. |
emulator | Target system — GBv1 (Game Boy) or GBCv1 (Game Boy Color). |
difficulty_decay | Difficulty decay rate in milliseconds per unit of threshold reduction (see §6). |
frames_per_segment | Number of frames in each segment. |
filters | Per-button input restrictions (see below). |
intro_frames | The first n generated inputs are forced to all-buttons-released. |
hold_frames | Each generated input is repeated for this many consecutive frames (see §4.2). |
The filters are ten independent boolean flags:
| Flag | Effect |
|---|---|
no-a / no-b / no-select / no-start | That button is never pressed. |
no-left / no-right / no-up / no-down | That direction is never pressed. |
no-socd | Simultaneous-opposing-cardinal-directions are cleared (Up+Down or Left+Right cancel). |
no-sss | Start+Select held together are cleared. |
For display the gateway also renders settings as a short human-readable string
— decay=<n>,frames=<n>[,<flags…>], for example
decay=1000,frames=3600,no-socd,no-sss,intro=100 — but this is a UI
convenience; the canonical, on-chain form is the CBOR blob.
These filters exist because unconstrained random input frequently produces degenerate behaviour — pausing the game, resetting via Start+Select, or jamming into a wall. Filtering them out makes meaningful progress dramatically more likely without sacrificing determinism.
Every gateway assigns run ids deterministically from the order of CreateRun
events on-chain: the first run is id 1, the second id 2, and so on, so all
gateways agree on ids without any coordination. For display, ids are rendered
as short alphabetic run codes (A, B, …, Z, AA, …); ids whose code
would begin with the reserved prefix SR are skipped during assignment so run
codes never collide with the protocol's own naming.
Only the ROM's keccak256 hash appears on-chain. The ROM itself is distributed out-of-band, and a gateway can only process runs whose ROM it actually holds.
Because indexing a run means emulating every submitted segment, gateways do not process every run they see — an operator enables a run locally to opt into it. Enabling requires the ROM to be present and the run's settings to decode. Events for disabled runs are still stored, so a run can be enabled at any later time and its entire derived state (segments, grades, coin balances) recomputed from the log; two gateways that enable the same run always converge on the same state.
Each segment's seed is derived deterministically from three values, concatenated and hashed:
seed = keccak256( parent_hash || recipient_address || salt )
parent_hash — the previous segment's state hash, or the run's
genesis hash for the first segment:
genesis_hash = keccak256( "Epstein didn't kill himself" || block_hash || log_index )
where block_hash and log_index locate the run's own CreateRun event.
Because the genesis hash depends on the block hash of the creation event, it
is unpredictable until the run exists on-chain, so miners cannot pre-mine
the first segment. The embedded phrase is a nod to Bitcoin's genesis-block
headline ("Chancellor on brink of second bailout for banks") in its
contemporary idiom — though where Satoshi's headline proved the block was
not minted before the newspaper ran, here it is the block hash that supplies
the freshness proof; the phrase is just flavour, fixed for all runs.
recipient_address — the address that will receive mining rewards.
salt — a value the miner is free to choose.
Because the seed commits to parent_hash, every seed is bound to a specific
point in a specific run; it cannot be replayed onto a different history.
A seed is expanded into controller inputs with a keccak256 hash chain. Starting
from state = seed, the indexer repeatedly computes
state = keccak256(state); each 32-byte digest yields 32 inputs, one per byte.
The eight bits of each byte map directly onto the eight Game Boy buttons:
bit 0 → A bit 4 → Right
bit 1 → B bit 5 → Left
bit 2 → Select bit 6 → Up
bit 3 → Start bit 7 → Down
The per-button filters are then applied to each input — any of the eight
buttons can be disabled, and SOCD or Start+Select combinations cleared, as
configured. If hold_frames is set, each drawn input is repeated for that many
consecutive frames, so only ⌈frames_per_segment / hold_frames⌉ distinct
inputs are pulled from the hash chain. Finally the first intro_frames inputs
are blanked to all-buttons-released. The result is a fully deterministic input
stream of frames_per_segment frames.
After replaying a segment, its state hash — the link consumed by the next segment — is:
segment_hash = keccak256( seed || state )
where state is the emulator's resulting state after the segment's frames.
Because seed already incorporates the previous segment's hash, the chain
stays linked without hashing the previous hash twice. This produces a
blockchain-within-a-blockchain: a tamper-evident chain of game states whose
integrity anyone can recompute from the public event log.
Progress is measured by novelty. As the emulator replays a segment, every frame receives exactly one grade, evaluated in order:
Comparisons span the entire run up to the evaluated frame, across all previous segments; segment boundaries have no effect on grading. A segment's score is its count of New frames — frames the run has never seen before. A high score means the seed pushed the game into genuinely unexplored territory.
This grading is what makes "progress" objective and machine-checkable, and it is the foundation of seed acceptance (§6).
To prevent the run from accepting trivial or low-effort segments, each submission must clear a minimum score threshold that decreases over time:
frames_per_segment — i.e.
every frame in the next segment must be New. This is essentially impossible
to meet immediately, forcing miners to search.difficulty_decay, expressed in milliseconds per unit of threshold
reduction.Concretely, the minimum acceptable score at time now_ms, for a segment built
on a parent timestamped base_timestamp, is:
elapsed = now_ms − base_timestamp·1000
max_score_needed = elapsed / difficulty_decay
min_score = frames_per_segment − max_score_needed (saturating at 0)
(If difficulty_decay is 0, the threshold is 0 and any segment is
accepted.)
Both timestamps come from the chain, so the check is deterministic: now_ms is
the block timestamp of the submitting transaction (not any gateway's wall
clock), and base_timestamp is the block timestamp of the submission that
produced the parent segment — or the run's creation, for the first segment. The
threshold therefore resets to its maximum every time a segment is accepted, and
every gateway evaluates every submission at exactly the same instant.
This creates a mining race with two opposing pressures:
The optimal strategy is to keep searching for high-scoring seeds and submit at the precise moment the decaying threshold dips below your best result. The harder miners search, the higher-quality the canonical run becomes — difficulty decay converts raw compute into game progress, the same way a hashrate converts compute into block security.
A submission is a single contract call:
submitSeed(to, run_id, parent, salt, score)
to is the reward recipient, parent is the segment hash the miner claims to
build on, salt completes the seed derivation of §4.1, and score is the
miner's self-reported score for the resulting segment. The contract records
all five values blindly; every gateway then runs the same validation pipeline,
ordered so that the cheap checks come first and the expensive emulation last:
parent must equal the current tip of the segment chain.
A submission built on anything else — most commonly a competing seed that
lost the race and now points at a stale tip — is discarded without
emulation.score must meet the decayed minimum of
§6.1, evaluated at the submission's block timestamp. Too-early submissions
die here, again without emulation.score; any mismatch rejects the submission and burns the
submitter's stake. A submission whose input stream crashes or wedges
the emulator fails deterministically on every gateway and is treated the
same way.to.The self-reported score is what makes this pipeline cheap to defend: without it, deciding whether a submission clears the threshold would itself require emulation, and anyone could force every gateway in the network to do unbounded work for free. With it, the only lie that costs validators an emulation is a well-formed submission whose score is false — and that lie is priced by the stake.
Note that only the chain's ordering decides races. When two miners both clear the threshold, the submission ordered first on-chain wins; the loser fails the parent check and is discarded cheaply, and its miner simply re-mines against the new tip.
Checks 1–3 of the pipeline cost a validator microseconds, so submissions that fail them are harmless noise. The one remaining attack is a submission crafted to pass the cheap checks and fail only after emulation — each such submission wastes a full segment replay on every gateway in the network. Staking makes that attack exponentially expensive:
The first offence is free (the requirement was zero), but it arms the mechanism: a sustained attack must acquire and burn coins at a rate that grows by two orders of magnitude per attempt, while an attacker who instead waits for the requirement to decay back to zero is rate-limited to roughly one wasted emulation per hour. The stake is checked against the submitting address, not the reward recipient, so it cannot be dodged by pointing rewards elsewhere. Burned stake only ever reduces a run's coin supply, so the supply-cap argument of §7.1.1 is unaffected.
Each run has an associated coin balance system. Coin movements are recorded
on-chain via transferCoin(to, run_id, amount), which emits an event; no
ERC-20 token value moves (coins can, however, be wrapped into a transferable
ERC-20 via the Wrapping — see §7.2). Balances are not stored on-chain — instead
each gateway derives them by applying mining rewards, TransferCoin
events, and stake burns (§6.3) in strict event order. A transfer whose
amount exceeds the sender's derived balance at that point in the log is simply
ignored, exactly like an invalid seed. Because balances are a pure function of
the event log, every gateway recomputes the same balances; there is no trusted
off-chain authority here.
The smallest indivisible coin unit is called a yuki; one coin = 10¹² yuki
(i.e. coins carry 12 decimal places). The unit is named after doujin creator
Yuki Nakai (中井ゆき). All amounts below are quoted in yuki unless a whole-coin
count is given explicitly. For convenience, 1,000,000 yuki is called a
marat, named after Marat Fayzullin.
i64 supply capGateways store coin balances as signed 64-bit integers in the smallest unit,
the yuki (1 coin = 10¹² yuki), so a run's total minted supply must never
exceed i64::MAX = 9,223,372,036,854,775,807. Three sources mint coins into a
run, and their amounts are deliberately tuned so that even the worst-case total
stays under that ceiling:
1,000,000,000,000,000 yuki (1000 coins), credited to
the run's creator from the CreateRun event itself. (Each gateway records
the credit when it enables the run — §3.2 — so every gateway that processes
the run derives the identical premine.)(BASE_REWARD >> era) × interval_seconds, where interval_seconds is the
time between this segment's accepting submission and the previous one (or
the run's creation, for the first segment),
BASE_REWARD = 36,404,315,848 yuki/second at era 0, and
era = ⌊(t − run_created_at) / ERA⌋ with
ERA = 126,144,000 s ≈ 4 × 365 days. Emission therefore tracks elapsed
time rather than segment count — slow, hard-fought segments mint more than
rapid-fire ones, and the rate cannot be inflated by producing segments
faster. The rate halves each era and is zero from era 63 on, so the total
emission is a geometric series summing to < 2 × BASE_REWARD × ERA.N lands at 3.5·N·(N+1) days. The densest era is the first (era
0 spans 1460 days): 3.5·19·20 = 1330 ≤ 1460 < 1470 = 3.5·20·21, so era 0
admits at most 19 deploys and every later era admits fewer. With the per-era
reward halving, total wrapper emission is bounded by
1000 coins × 19 × Σ2⁻ⁿ ≤ 1000 coins × 19 × 2 = 38,000 coins.BASE_REWARD is chosen by subtracting the creator reward and the wrapper bound
from i64::MAX, then floor-dividing the remaining budget across the segment
series:
| Term | Value (yuki) | Notes |
|---|---|---|
i64::MAX | 9,223,372,036,854,775,807 | signed-64-bit supply ceiling |
| − creator reward | 1,000,000,000,000,000 | 1000 coins |
| − wrapper bound (≤) | 38,000,000,000,000,000 | 1000 coins × 19 deploys/era × Σ2⁻ⁿ ≤ 2 |
| = segment-emission budget | 9,184,372,036,854,775,807 | remaining for segments |
giving BASE_REWARD = ⌊budget / (ERA × 2)⌋ = 36,404,315,848 yuki/second. The
exact grand total — the creator reward plus every segment's emission plus the
full escalating wrapper schedule, all halving per era — is
9,211,678,530,883,416,398 yuki, below i64::MAX with headroom ≈ 1.2 × 10¹⁶
yuki.
Because the real geometric segment series sums to strictly less than 2 and the
wrapper count/cadence assumptions are conservative over-estimates, the actual
per-run supply is guaranteed to remain below i64::MAX for all time — a single
run's coin balances can never overflow.
SeedrunWrapper.sol, SeedrunToken.sol)Coins normally live only inside gateways. The Wrapping is the bridge that
lets a run's coins leave the gateway as a standard, transferable ERC-20 — and
return. All wrapping activity flows through one fixed, indexed SeedrunWrapper
contract which, like Seedrun, only emits events; the gateway decides what is
canonical. Wrapping happens in rounds, one SeedrunToken ERC-20 minted per
round:
enroll(run_id, amount) to move coins
from their balance into the pending round's pool, or unenroll(...) to pull
them back; the gateway tracks the pool off-chain. Enrollment for a round
freezes once its window elapses, so a pending deploy's snapshot cannot be
front-run. The window starts at 7 days for the first Wrapping and grows
by 7 days for each subsequent one (7d, 14d, 21d, … — uncapped); it is
overridable to a flat value via an environment variable for testing.deploy(...) to launch
the round's SeedrunToken and announce its arguments. The gateway treats a
deployment as canonical only if every argument matches what it independently
computes: the round id, an enrollmentRoot Merkle-committing the frozen
pool, a prevRoot committing this run's earlier round tokens, the halving
reward, and the token's name (Seedrun <code> #<n>) and symbol
(SR<code><n>). A canonical deploy burns the pooled coins off-chain — they
now exist as the claimable ERC-20 — and pays the deployer the 1000-coin
wrapper reward (§7.1.1).claim their
allocation with a Merkle proof against enrollmentRoot; holders of an
earlier round's token for the same run can upgrade 1:1 into the new one
with a proof against prevRoot.SeedrunWrapper (re-emitting from its fixed address, so the gateway need
not watch every token address), and on seeing the deposit the gateway
credits the coins back to the sender's off-chain balance. Only tokens the
wrapper itself deployed are honoured, so a forged ERC-20 cannot mint coins
out of thin air.Wrapping shuffles coins between off-chain balances and the on-chain ERC-20 but
never changes a run's total minted supply, so the i64 cap of §7.1.1 still
holds.
| Contract | Role |
|---|---|
Seedrun | Thin event log: createRun(settings), submitSeed(to, runId, parent, salt, score), transferCoin(to, runId, amount). No validation, no access control. |
SeedrunWrapper | Fixed, indexed event log for the Wrapping: enroll, unenroll, deploy, notifyTransfer. No validation. |
SeedrunToken | The ERC-20 a single Wrapping round mints — a 1:1 (12-decimal) wrapper of a run's coins, with Merkle claim/upgrade; transfers back to the token unwrap (burn) it. |
The Seedrun and SeedrunWrapper contracts are intentionally minimal — their
functions do nothing but emit. This is a feature: by refusing to encode any
rules on-chain, they keep gas costs low and make the off-chain rule set the
single, swappable source of truth.
Seedrun's trust assumptions are deliberately narrow:
TransferCoin
events, and stake burns), so every gateway recomputes and audits them
independently.The net effect is a system where the fun, expensive part (mining seeds, replaying games, rendering frames) happens off-chain and cheaply, while the part that needs to be neutral and permanent (ordering of intents) happens on-chain and minimally.
The reference Seedrun gateway is written in Rust and bundles indexing, mining, transaction submission, and the web UI into a single binary:
boytacean Game Boy / Game Boy Color core.alloy), decodes the CBOR run
settings (ciborium), runs the validation pipeline of §6.2 (including
stake enforcement), replays segments, grades and scores them, maintains
the canonical run, derives coin balances, and settles Wrapping events.submitSeed once a
mined seed clears the decaying threshold — using the gateway's local wallet
(alloy-signer-local, a BIP-39 mnemonic stored per node).StandardMerkleTree-compatible
roots and proofs for Wrapping enrollment and claim/upgrade.env-libvpx-sys, webm-iterable) with Opus audio (audiopus) for
per-segment previews.sqlx) for runs, segments, and coins.axum + maud) for creating runs, mining, and browsing
frames.Seedrun is experimental software. Nothing here is financial advice.