← Instrument

Notes on method

Darkflow is built against a synthetic generator, and it is built so that when the data becomes real, nothing on the screen will need to be taken on trust. Five working rules, and then the list that produced them.

  1. Everything is measured in pixels before and after a change. Luminance on a scanline, contrast ratios, widths of panel groups. Nothing is adjusted by eye, because eyes are what every failure below got past.
  2. Zero is a claim. Without enough data the instrument shows —, not a value. The headline figure is null until a block has been classified, and every figure derived from it inherits the null.
  3. Nothing is drawn that the data does not support. The range band waits for twelve blocks. The fee axis waits for 256 samples before it places a tick. Private flow is not marked until the pool has been watched for five blocks or seeded from a snapshot.
  4. A green test with no dead mutant is not evidence. The source is mutated to check that the test fails, and fails for the right reason. One solver below survived every mutation, which is how its fallback was found to be hiding it.
  5. Measured and inferred are kept apart. The panel says “never seen by this feed”, not “never in the mempool”, because the second is a statement about the network and the first is what was recorded.

Failures that looked right

The characteristic failure of this project is one that cannot be seen, because it produces a plausible picture. None of the entries below was reported by a viewer; each was found by measuring something that looked fine. For each: what the screen showed, why nothing flagged it, and how it was caught.

  1. 01A memory sized to block time

    Showed
    Hashes were remembered for three block times. Blocks reported up to 59% never seen against a generator that never exceeds 40%: the record had forgotten transactions that were still in the pool, and each one landed as private flow.
    Silent because
    59% is a plausible figure for a block. Nothing about the picture was wrong except the number, and the number had no reference.
    Caught by
    The probe compares the classifier against the generator’s own count of what it never announced. The two disagreed on every block.
    lib/seen.ts · tests/synthetic.test.ts
  2. 02Flights from the wrong pool

    Showed
    When a block landed, transactions flew from the 300-mark render sample instead of from the record of what had been seen. About 12% of a block’s public transactions had a mark to fly from — exactly the fifteen reserved for the highest fees, every block — so the other 88% appeared in place, which is the picture of private flow.
    Silent because
    Rows appearing in place is what the product is supposed to show. The animation was doing precisely what it was told.
    Caught by
    Counting which rows arrived with a trajectory and comparing that to the count of public transactions in the block.
    lib/seen.ts · lib/canvas/world.ts
  3. 03An alpha that was NaN

    Showed
    Block marks were given firstSeen = NaN, on the reasoning that a transaction never announced has no earlier sighting. A NaN that reaches ctx.globalAlpha is ignored by the canvas, so every affected mark drew at full brightness — through weeks of verification captures that all looked fine.
    Silent because
    The canvas does not throw on NaN. It keeps the previous alpha, and the previous alpha was a perfectly good one.
    Caught by
    Measuring the decay along a scanline instead of looking at it. Marks that should have faded to the floor were at the ceiling.
    lib/canvas/world.ts · lib/canvas/draw.ts
  4. 04A solver that could not be wrong

    Showed
    The cubic-bezier easing solves x(t) = progress with Newton–Raphson and falls back to bisection. Flipping the derivative’s sign, swapping its coefficients, or reversing the Newton step left the entire easing suite green.
    Silent because
    The bisection fallback is a complete solver on its own. It rescued every error Newton could make, at about thirty extra evaluations per call, and changed no result.
    Caught by
    Mutation testing. The derivative is now exported and tested directly against a numerical one, so the fallback can no longer hide it.
    lib/easing.ts · tests/easing.test.ts
  5. 050.0% on the block that closed calibration

    Showed
    The warm-up closes on the fifth block, and that block was classified before it counted: its unrecognised hashes were “unknown”, not “ghost”. Reading the ghost count once warm found none, and the panel printed 0.0% never seen for twelve seconds.
    Silent because
    A zero looks like a reading. It is the product’s claim made backwards, and it stood on the panel with the same authority as a true one.
    Caught by
    A test that walks the calibration block by block and asserts null, not zero, until a block has actually been classified.
    lib/readout.ts · tests/calibration.test.ts
  6. 06A green dot reading LIVE over a PRNG

    Showed
    The status indicator said live while every figure on the page came from a seeded generator. The name of the active source existed only in a console.log behind a debug flag.
    Silent because
    Health and provenance are different questions, and the dot answered the first. The stream was healthy. It was also not Ethereum.
    Caught by
    Reading the page as a stranger would. The dot now says simulated data and the About plate says it again in words.
    components/StatusIndicator.tsx · components/About.tsx
  7. 07A range band from two blocks

    Showed
    The spread under the headline figure was a min/max over however many blocks had been classified. Measured over 600 generated blocks whose true spread is 30 points, two samples recover 33% of it, five recover 64%, twelve recover 82%.
    Silent because
    A narrow band does not read as thin evidence. It reads as a stable figure, which is a claim about Ethereum that nobody measured — and the band widens so slowly that nothing announces the earlier picture was wrong.
    Caught by
    Running the estimator against the generator at every sample count. The band now waits for twelve blocks and is labelled with the count it was built from.
    lib/readout.ts
  8. 08A block shuffled for realism

    Showed
    The generator interleaved private flow into the block with a Fisher–Yates shuffle, which spread it correctly and destroyed the fee ordering of everything.
    Silent because
    The block column shares its top, bottom and extent with the chamber, whose vertical axis is the priority fee. Next to it, the block’s vertical axis meant nothing, and nothing on screen said so.
    Caught by
    Two adjacent rows 0.18 gwei out of sequence, noticed while checking a different thing. The block is now sorted by tip, and private flow is placed by resampling the tips it sits among.
    lib/stream/synthetic.ts
  9. 09An axis labelled with the wrong quantity

    Showed
    The chamber’s note said “height = fee offered”. Heights were computed from the effective priority fee, a different number that mostly would not fit on the axis.
    Silent because
    Both are fees, both are in gwei, and the picture looked the same under either caption.
    Caught by
    Six live marks read against the axis: offers ran 12 to 31 gwei while the axis spanned 0.1 to 12.4. The height tracked the priority fee every time.
    components/Instrument.tsx · lib/fees.ts
  10. 10A rank held by a number that had stopped being true

    Showed
    Each mark cached its priority fee when it appeared, and the quota that keeps the highest bids on screen ranked by that cache. Legacy transactions bid a single gas price, so their priority fee moves every time the base fee does. The quota did not.
    Silent because
    Every mark sat at the height its live fee put it, so the picture was consistent with itself. Only the choice of which marks were on screen at all was made from stale numbers, and an absence has no pixels.
    Caught by
    A test that moves the base fee one step and asserts the reservation follows the new order. It did not, and the cache went. Every rank now asks the fee at the moment it is compared.
    lib/canvas/world.ts
  11. 11Two thirds of the pool on one row

    Showed
    Every transaction offering nothing above the base fee landed on the same bottom pixel row of the axis, a dense line that read as the floor of the scale.
    Silent because
    The axis is logarithmic and clamps; nothing tells a clamp that most of its input has hit the stop. The line looked like an edge of the instrument, not like data.
    Caught by
    Counting the recording: 65.8% of pending transactions at or under the floor, and 12% of those included against 78% of the rest. That population is a category, not a height. It now has a band of its own, and the axis starts above it.
    lib/canvas/layout.ts · lib/canvas/world.ts
  12. 12A fix aimed at the wrong reading

    Showed
    BASE FEE, in the panel, wrapped onto two lines at 1424 px. The obvious cause was a reading added the same day, and removing it would have looked like a repair.
    Silent because
    The column was 143.6 px wide with or without that reading; the wrap came from the legends and the group widths. Taking the reading out would have left the wrap one resize away, with the story already closed.
    Caught by
    Measuring the column before removing anything. The legends no longer wrap, the groups are rebalanced, and the panel was checked at 1280, 1424 and 1536 for text that touches.
    components/Instrument.tsx · app/globals.css
  13. 13A governor that watched the average and missed the stutter

    Showed
    On an emulated phone with the CPU held at a sixth of its speed, the field dropped two frames a second — 40 to 107 ms each — while the render budget stayed exactly where it started.
    Silent because
    The governor judged a window by its mean frame rate, and the mean was 67 fps: twenty-seven fast frames absorb one slow one. Nothing it measured could see the frame a reader sees.
    Caught by
    Recording every frame’s duration instead of their average. The slowest frame per window now reaches the governor beside the mean, and either one trips it; the same run cuts the budget by a third within twenty seconds.
    lib/capability.ts · lib/canvas/engine.ts
  14. 14A condition that could never be false

    Showed
    The first draft of that fix also demanded “no stutter” before giving capacity back. The tests passed. So did the test with that clause deleted.
    Silent because
    The clause sat in an else branch that the stutter check above it had already excluded. Reachable in the reading, unreachable in the running; a mutant is the only reader that notices.
    Caught by
    Mutation testing: the mutant survived, which meant the code it removed did nothing. The clause went, and the comment now says why the guard is implicit.
    lib/capability.ts
  15. 15A bar waiting for something that was not coming

    Showed
    During warm-up the calibration bar read “waiting for the pool snapshot · block 1/5 without it”.
    Silent because
    It was true of the generator and of the first fake ingest, both of which send one. The real ingest, by decision, never does, and the recording was captured from one that did not. The words were inherited, not checked.
    Caught by
    A screenshot taken for a different reason. The bar now counts blocks, and the prop that made it promise a snapshot stays for a source that can keep the promise.
    components/CalibrationBar.tsx · components/Instrument.tsx
  16. 16A fix undone by the tool meant to undo a mutant

    Showed
    Nothing on the screen: a working tree in which the governor fix above had quietly reverted to the version from three weeks earlier.
    Silent because
    To measure the old behaviour against the new, the fix was mutated with sed and then restored with git checkout — which, in a tree where nothing is committed, restores the last commit, not the last edit. The tests were green before and after, on different code.
    Caught by
    A grep for the new constant, done out of habit before the next run, found nothing. The fix was rebuilt from its own notes and re-verified from zero; the rule since is that a mutant is undone with the inverse edit, never with checkout.
    lib/capability.ts
  17. 17A recording that lost four blocks and looked complete

    Showed
    A fresh five-minute recording: twenty-one blocks, real transactions, every block whole. Played back, nothing on screen suggested that twenty-five had landed.
    Silent because
    The capture retried a failed request but not a successful one with the wrong answer: a public gateway serves a fresh head from an upstream a block behind, whose receipts are missing for some hashes. That parses to nothing, and “nothing” was logged as a bad shape and dropped, four times in five minutes.
    Caught by
    Counting the block numbers in the file against the first and last, which the recording itself records. The parse now sits inside the retry; the next capture kept all twenty-five.
    scripts/capture-replay.mjs
  18. 18A build gate that read the lines it expected

    Showed
    “✓ Compiled successfully”, printed at the end of every check for several hours, while the production build had been failing since the live feed’s route handlers were added.
    Silent because
    The gate piped the build’s output through a filter for “error”, “Failed” and the success line. The build compiled, then died two steps later on “⨯ Invalid segment configuration export” — a route exporting maxDuration as an imported constant where the framework requires a literal — and that line contains none of the three words. The exit code was never read.
    Caught by
    Trying to start the production server for a screen recording: no BUILD_ID. The route exports a literal now, a test holds it equal to the constant the stream limit derives from, and a gate is its exit code — the output is for reading, not for deciding.
    app/api/[endpoint]/route.ts · the verification rig
  19. 19A status endpoint describing a process that was not there

    Showed
    First deployment. The stream delivered frames and the page went live; /api/state, asked the same second, reported running: false and zero starts, and the panel’s COVERAGE reading stayed blank.
    Silent because
    The three paths were three route files, and on this platform a route file is its own function with its own instance. The core lives in module state. The stream’s function had one; the state’s function had never started one, and answered truthfully about the wrong process. Locally, where everything is one process, the three had always agreed.
    Caught by
    Reading /api/state and /api/stream from outside within the same minute and noticing they could not both be true. One dynamic route now serves the three paths from one function.
    app/api/[endpoint]/route.ts

What the feed measured

Three numbers the feed had to produce before it could be called live, measured on 2026-09-03 from a development machine running the same core the site runs. The host’s own figures replace these once it is deployed; until then they are the only ones there are.

Announced
12 to 14 transactions a second 11.9/s over a five-minute probe of the feed; 14.3/s over a twenty-minute run of the ingest (16,850 in 1,175 s). Mainnet as one public endpoint hears it, not as the chain produces it.
Coverage
about half of a block heard before it landed 55.5% over fifteen blocks in the probe; 47% to 56% across the day’s runs by the ingest’s own count, block by block. A second feed with three times the announcements added one point, so what is left unheard is mostly private flow, not deafness.
Blocks
every head became a block 97 of 97 over twenty minutes, receipts had for all, none missed, none invented; one link event, the opening one. Later runs: 12 of 12, 7 of 7.

Open