Seedrun: A Whitepaper 🌱

Collaborative, verifiable game progress through randomness mining.


Abstract

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.


1. Motivation

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.


2. System Overview

Seedrun has two layers:

LayerResponsibility
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.


3. Runs and Segments

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.

3.1 Run settings

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:

FieldMeaning
romkeccak256 hash (32 bytes) identifying the exact game ROM.
emulatorTarget system — GBv1 (Game Boy) or GBCv1 (Game Boy Color).
difficulty_decayDifficulty decay rate in milliseconds per unit of threshold reduction (see §6).
frames_per_segmentNumber of frames in each segment.
filtersPer-button input restrictions (see below).
intro_framesThe first n generated inputs are forced to all-buttons-released.
hold_framesEach generated input is repeated for this many consecutive frames (see §4.2).

The filters are ten independent boolean flags:

FlagEffect
no-a / no-b / no-select / no-startThat button is never pressed.
no-left / no-right / no-up / no-downThat direction is never pressed.
no-socdSimultaneous-opposing-cardinal-directions are cleared (Up+Down or Left+Right cancel).
no-sssStart+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.

3.2 Run identity, ROMs, and enabling

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.


4. Seeds and the Hash Chain

4.1 Seed derivation

Each segment's seed is derived deterministically from three values, concatenated and hashed:

seed = keccak256( parent_hash || recipient_address || salt )
  1. 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.

  2. recipient_address — the address that will receive mining rewards.

  3. 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.

4.2 Input expansion

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.

4.3 Segment hashing

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.


5. Frame Grading and Scoring

Progress is measured by novelty. As the emulator replays a segment, every frame receives exactly one grade, evaluated in order:

  1. Repeat — identical to the immediately preceding frame (paused menus, static screens, idle animation loops).
  2. New — the first time this exact frame has appeared anywhere in the run.
  3. Existing — a frame seen earlier in the run but not the immediate predecessor (e.g. revisiting an area).

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).


6. Mining: Difficulty, Submission, and Acceptance

6.1 Difficulty decay: mining as timed proof-of-work

To prevent the run from accepting trivial or low-effort segments, each submission must clear a minimum score threshold that decreases over time:

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.

6.2 Submission and the validation pipeline

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:

  1. Stake gate (§6.3) — the submitting address must hold the currently required stake in the run's coins, or the submission is ignored without further work.
  2. Parent checkparent 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.
  3. Threshold check — the claimed score must meet the decayed minimum of §6.1, evaluated at the submission's block timestamp. Too-early submissions die here, again without emulation.
  4. Emulation — only now does the gateway expand the seed, replay the segment, and grade its frames. The computed score must exactly equal the claimed 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.
  5. Acceptance — the first submission in chain order to pass every check becomes the run's next segment; its frames are graded into the canonical run and the mining reward (§7.1.1) is minted to 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.

6.3 Stake: pricing expensive spam

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.


7. Coins and Wrapping

7.1 Coins

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.

7.1.1 Reward schedule and the i64 supply cap

Gateways 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:

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:

TermValue (yuki)Notes
i64::MAX9,223,372,036,854,775,807signed-64-bit supply ceiling
− creator reward1,000,000,000,000,0001000 coins
− wrapper bound (≤)38,000,000,000,000,0001000 coins × 19 deploys/era × Σ2⁻ⁿ ≤ 2
= segment-emission budget9,184,372,036,854,775,807remaining 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.

7.2 Wrapping coins: the Wrapping (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:

  1. Enroll / unenroll. Holders call 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.
  2. Deploy. After the window closes, anyone may call 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).
  3. Claim / upgrade. The token is a 1:1 wrapper of coins, using 12 decimals to match the coin's smallest unit, the yuki. Enrollees 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.
  4. Unwrap. Transferring the ERC-20 back to the token contract's own address burns the deposit on receipt. Every token movement signals the 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.


8. Smart Contract Summary

ContractRole
SeedrunThin event log: createRun(settings), submitSeed(to, runId, parent, salt, score), transferCoin(to, runId, amount). No validation, no access control.
SeedrunWrapperFixed, indexed event log for the Wrapping: enroll, unenroll, deploy, notifyTransfer. No validation.
SeedrunTokenThe 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.


9. Trust Model and Verifiability

Seedrun's trust assumptions are deliberately narrow:

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.


10. Reference Implementation

The reference Seedrun gateway is written in Rust and bundles indexing, mining, transaction submission, and the web UI into a single binary:


Seedrun is experimental software. Nothing here is financial advice.