# The live feed contract One JSON shape. The chain scene (`scene/live-dag.js`, `scene/proof-core.js`) reads it, and two producers write it: | Producer | Where | Source of the rows | |---|---|---| | The observer | `site/api/live.mjs` (GET `/api/live?window=N`, igneum.network) | Neon tables the observer (`tools/observer/observer.mjs`) fills from node 1 | | The miner app | `app/igneum-app/src/live.rs` (GET `api/live?window=N` on the engine's port) | The observer's reply, passed through; when the observer is unreachable, the local node's `igneum_getRecentBlocks` shaped into the same rows (`partial: true`) | The machine-readable key lists live in `scene/feed-contract.json`. `tools/scene/feed-contract.mjs` validates a reply against them (the gate runs it over the recorded fixture and over known-bad shapes); the app's Rust test reads the same file, so neither side can add a field or drop one without the other noticing. ## Top level | Key | Type | Meaning | |---|---|---| | `ok` | true | A reply with `ok: false` carries `error` and nothing else | | `now` | ISO 8601 string | The producer's clock when the reply was built | | `state` | object | The network state, below | | `blocks` | array | The window's blocks, oldest first, at most 1,800 | | `miners` | array | Vote keys seen in the last 10 minutes | | `events` | array | The last 30 observer events | | `finality` | object | Checkpoints and weights | | `proving` | object | The proving layer | | `partial` | true, app only | The rows came from the local node: no parents, no proof state, no checkpoints | ## `state` `stale`, `age_s`, `network`, `node_version`, `height` (the chain block number), `block_count`, `header_count`, `blue_score`, `difficulty`, `hashes_per_second_estimate`, `peers`, `mempool`, `blocks_60s`, `blocks_per_second_60s`, `blocks_per_minute` (10 numbers, oldest first), `miners_10m`, `observer_started_at`, `observer_lag_s`, `queue_depth`, `updated_at`. A value the producer does not know is `null`, never missing. App extensions, additive and ignored by the scene: `source` (`observer` or `node`), `you_blocks` (this machine's blocks in the window), `node` (the local node's own numbers for the Cards tab). ## A block | Key | Type | Meaning | |---|---|---| | `hash` | string, the first 16 hex | Both producers cut the hash the same way, so a hash reads the same everywhere | | `number` | number or null | The chain block number once planned; null off the chain or before the plan | | `blue_score`, `daa` | number or null | | | `ts` | number | Header time in milliseconds | | `rx` | number | When the producer first saw the block, milliseconds | | `parents` | array of hash strings | Empty on a `partial` reply | | `chain` | boolean | On the selected chain | | `miner` | string, 8 hex | The vote key id. NEVER a word: the app marks its own blocks through the scene's `mine` option, not by rewriting this field | | `color` | `blue`, `red` or `pending` | GHOSTDAG inclusion | | `locked` | boolean | A locked checkpoint block | | `final` | boolean | At or before the newest lock | | `shards` | array of `{i, n, state, prover, lag, payout, pgas}` | `state` is `planned`, `proving`, `verified` or `paid` | | `proven` | boolean | Every shard verified or paid | ## `miners[]`, `events[]`, `finality`, `proving` A miner is `{id (8 hex), blocks, share (percent of the 10 minutes), last_seen (ISO or null), engine (string or null)}`. An event is `{ts, kind, text}`. `finality` is `{supported, active, next_index, latest_locked_index, latest_locked_hash, latest_locked_blue_score, params, weights, checkpoints}`; a checkpoint is `{index, hash, blue_score, daa, state (pending or locked), signed, active, total, fraction_active, fraction_total, votes, voters, locked_at, updated_at}`. `proving` is `{supported: false, reason}` or the supported object (`active`, `activation_daa`, `tip_daa`, `verifier`, `pool`, `paid_shards_total`, `shard_budget_pgas`, `blocks_10m`, `blocks_fully_proven_10m`, `shards_proven_10m`, `shards_paid_10m`, `median_proof_lag_s`, `provers_10m`). ## What the scene does with it `IgneumDag.mount(canvas, opts).push(reply)` normalises every block (unknown colours become `pending`, unknown shard states `unknown`, a hash over 256 characters or a non-numeric `ts` drops the row), keeps the newest 340 s, and paints at once. The window the scene shows (`opts.window`, 30 to 300 s) is independent of the window the page asks the producer for: both the site and the app ask for 300 s so the viewer can pan and zoom out to the full range (`?window=300`). ## The recorded fixture `scene/fixtures/live-2026-10-07.json` is one real `/api/live?window=300` reply (7 October 2026, 13:17 UTC, 905 blocks, 15 miners, 40 checkpoints, 14 proven blocks, 50 excluded). The contract test validates it; the parity test renders it through both surfaces; the paint test pushes it into a hidden document.