# Caverio companion read model: contract v3.4

*v3.4: W-contract-v34, 2026-09-28 morning Beirut. One vocabulary: Vesper's 10:00 ruling keeps the v3.3 names (`now.history {since, kind}`, `marketCap.basis: estimate`, anchor `previous_material`, PriceRead `native`/`usd`), and the v3.2 names are refused wherever they appear, each with a reason that names it (§0.19; V66 to V69). The validator's run-time rename map is gone; the one-time migration is `migrate_v32_to_v33.py`. The page minute is defined exactly (§7.1; V70). Board's entry cap has a field and a callable (§8 `entryCap`, `board.py`; V71), admitted for `board.json` when Board gets its v3.3 read model and present tonight only in `staging/board.json`. No document shape changes: `contractVersion` stays `companion-v3.3` (§11). Sections changed are marked **v3.4** in place. `CHANGES-v3.4.md` has one row per item. Not admitted: Bolo re-probes first. v3.3 as it stood before the first edit is frozen in `legacy-v33/`.*

*v3.3: W-contract-v33, 2026-09-28 00:20 Beirut onward. Bolo's two B6 release holds (entrants rewritten by late evidence; a source clock after its availability admitted and shown as Latest), the price boundary at the callable, the canonical names and the price-move anchor of the 23:35 pin (`history.since`, `estimate`, previous material event), and the B2 seam: a typed native quote with an explicit USD basis, `surfacedAt` as a Field, and an empty state history for a prospective store. Sections changed are marked **v3.3** in place. `CHANGES-v3.3.md` has one row per change with file, line, vector, before and after. Not admitted: Bolo re-probes first, and no field ships before that. `contractVersion` is `companion-v3.3`. The v3.2 text, code and schema as Bolo re-probed them at B6 (his `RESULT.json` `sourceHashes`) are frozen in `legacy-v32/`.*

*v3.2: W-contract-v32, 2026-09-27 23:37 Beirut onward. Bolo's four pass12 semantic blockers and the pass12 hardening fixed, and §7 rewritten as the frozen Now row (DESIGN-FREEZE §2, pass 7 NOTES (b)), with the three definitions and two pins closed with Bolo at 23:35 (§12.4). Sections changed are marked **v3.2** in place. `CHANGES-v3.2.md` has one row per change with file, line, vector, before and after. Not admitted: Bolo re-probes first, and no field ships before that. `contractVersion` is `companion-v3.2`. The v3.1 text, code and schema as Bolo reviewed them (pass13 hashes) are frozen in `legacy-v31/`.*

*v3.1: W-contract-v31, 2026-09-27 morning, Bolo's five blocking findings fixed (`CHANGES-v3.1.md` has one row per finding, with file, line, vector, before and after). Not admitted: Bolo reruns his adversarial probes first, and no field ships before that. The file keeps its v3 name so his probes find it; `contractVersion` is `companion-v3.1`. The v3 text as reviewed is frozen in `legacy-v3/`.*

*v3: W-contract-v3, 2026-09-27 01:48 to about 03:15 Beirut. For Bolo's review before any field ships (overnight order item 5). Nothing here is live. Decision record: `reviews/2026-09-27-vesper-read-of-external-review.md` ("the read"). Where the read, T5 and T8 disagree, the read governs, then T5, then T8; every such choice is listed in §12.*

Folder: `contract/`. Schemas `*.schema.json` (JSON Schema draft 2020-12), fixtures `fixtures/*.json`, test vectors `state-vectors.json`, the projection rule as code `projection.py`, validator `validate.py` (last run in `VALIDATE.out`). The fixtures and schemas are generated by `make.py` and `vectors.py` from the real T5 data files, so a number in a fixture is never retyped by hand.

---

## 0. Conventions

Inherited from v2 (`signal-room/contract/room.schema.md`, 2026-09-10), unchanged:

1. Every timestamp is ISO 8601 UTC with a `Z` suffix.
2. Unavailable means null with a reason, never zero. A computed zero is a value; a missing number is not.
3. Addresses are exact literals from the source. EVM addresses are lower-cased only inside `key` and `situationId`.
4. No composite score is ever emitted. Sort inputs are emitted as their parts (§7) and printed in words, never as one number.

New in v3:

5. **One nullable shape everywhere.** Every field that can be null is a `Field` object: `{ "value": ..., "reason": ... }`, plus optional provenance keys (`basis`, `source`, `asOf`, `unit`, `note`, `illustrative`, `switchNote`, `replacement`). The schema enforces: `value` null if and only if `reason` is a non-empty string. Fields that can never be null are bare values. There is no sibling `<field>Reason` anywhere.
6. **Three clocks per event** (T2 §2.3): `providerAt` (the source's clock: block time, message time; a `Field`, null with reason when the source gives none, never copied from ours), `receivedAt` (our receipt), `availableAt` (when it became usable to a member). **States, diffs, lights and relationships order by `availableAt`**, so a replay shows what a member could have known, when. Pages print `providerAt` when present.
7. **`basis` on every price-bearing number** (unit `usd`): `tape` (our own swaps), `rpc` (our balance or supply reads), `intake` (our intake rows), `user`, `derived`, or `provider:<name>`. Non-price numbers carry `basis` too wherever it is known.
8. **The Q1 switch-date rule**, printed verbatim in every provider field's `switchNote`: *"Provider field: displayed during the build and removed or replaced at the Q1 switch date (own tape only before the first outsider sees a page). Nothing may depend on it."* Each provider field also names its `replacement` (the tape field that takes its place, or `none: removed at the switch`). A consumer that hides every `provider:*` field must still render a complete page.
9. **Sentences describe, never instruct.** No verdict words, no "buy", "sell", "score", "smart money", "alpha", "edge", "bullish", "bearish". Sentences carry durations ("58 min ago"), never wall-clock times, so the page localises clocks from the structured `at` fields.
10. **Caller names only in `member` blocks** (`{ "memberOnly": true, "callerName": ... }`, Q4, C5). The public projection drops the block. No caller quote appears anywhere in the payload: a caller event carries its time, channel (members only) and stance label, never text.
11. **`illustrative: true`** at the field, event, light or row marks fixture values that are invented. Live payloads never carry it.

New in v3.1:

12. **`asOf` precision and the knowable-later rule.** `meta.asOf` and `situations[].asOf` are to the second. Evidence is knowable at `asOf` when its `availableAt` is at or before `asOf`, compared to the second; nothing is rounded up to the minute. Pages may print minutes, but they never round `asOf` forward to admit an event. SPRING is frozen at 17:27:30Z, so the minute-60 read (available 17:27:06Z) is knowable and §12.2 item 4 and the fixture agree.
13. **Availability gate.** Before the projection runs, every input is checked against `asOf`: an event or light with `availableAt` after `asOf` (a negative age), or with no `availableAt`, is not admitted. It is listed in `state.inadmissible[]` with its reason (§10) and never reaches a sentence (§3.7).
14. **Stable source-event keys.** Every event carries `sourceKey` = `source|sourceId`, or `source|sha256` of the raw payload when the source gives no id. The situation id is `chain:address#openedAt~k`, `k` = the first 8 hex of sha256 of the opening event's `sourceKey`, so two reopenings in the same second get different ids (Bolo M2 item 7). `lifecycle.openingKey` prints the key.

New in v3.2:

15. **Strict clocks** (`clocks.py`). Every clock the gate compares is parsed strictly and never raises. ISO 8601 with `Z` or an offset in `[-14:00, +14:00]` with minutes 00 to 59 is accepted and converted to UTC. An offset-naive clock is refused with `evidence clock has no UTC offset; not admitted`; an offset such as `+00:60` or `+14:30` is refused with `evidence clock offset is not a valid UTC offset; not admitted`, never normalised (v3.1 read `+00:60` as `+01:00`). V55 to V57.
16. **Adapter checks** (`adapter.py`). JSON Schema checks shape, not arithmetic. Derived values are checked at the producer adapter boundary; a schema-valid row that fails is refused with a reason and never reaches a page (§2.5, V58).

New in v3.3:

17. **Clock order** (`clocks.admit_pair`, Bolo B6 item 2). Every admission of evidence that carries a source clock and an availability clock goes through one shared check: material events, price reads and their USD conversion, state history entries, the safety event and the retention cohort (when they carry both), and supply reads (`observedAt`, `availableAt`). A source clock after its availability is refused with `evidence clock after its availability; not admitted`. A material event whose anchor clock is after the event is refused with `anchor after the event; not admitted` (`clocks.admit_anchor`). The JSON Schema does not compare clocks; this is a runtime check, and the fields a page reads at `t` use only admitted evidence (V60).
18. **Price strings at the callable** (Bolo B6 hardening). Before any decimal arithmetic, every price string (`native.amount`, `usd.value`, a conversion `quote`) must be a finite positive decimal string matching the schema pattern; otherwise `price is not a finite positive decimal; not admitted`. `NaN`, `Infinity`, `-1` and `0` are refused (a zero price is not a read, and it would be a divide-by-zero baseline). The schema refuses zero too (V61). Curve reserves get the same pattern check at the adapter (`reserve is not a canonical decimal; not admitted`); a zero reserve stays admissible, because a reserve is not a price and nothing divides by it.
19. **One vocabulary** (**v3.4**). The v3.2 names are not aliases; they are refused, and the reason names the offending key or value and the v3.3 name: key `historyFrom` on a Now document (`history.since`), value `assumed_supply` under `basis` (`estimate`), value `previous_same_kind` as an anchor kind (`previous_material`), the threshold sentence `>= 25% against the previous price move` (`>= 25% against the price at the previous material event`), and key `price` on a price read (`native.amount`, `usd.value`). `nowat.dead_names` finds them; `now_at` refuses a document carrying one and lists a row carrying one under `refusals[]` with the path; `admit_price_read` and `admit_material_event` refuse with the same reasons (§10; V66 to V69). A v3.2 document is migrated once with `migrate_v32_to_v33.py`, never read through a map. The schema refuses the same five (enum, `additionalProperties`, and a `not` on `threshold`).

## 1. Top level

```
GET /api/companion/situation/{situationId}   -> { meta, situations[1], now: null, board: null, market: null, capabilities, warnings }
GET /api/companion/now                        -> { meta, situations[],  now,       board: null, market,      capabilities, warnings }
GET /api/companion/board                      -> { meta, situations[],  now: null, board,       market: null, capabilities, warnings }
```

One shape, `{ meta, situations[], now, board, market, capabilities, warnings }` (`companion.schema.json`), served as three documents. `now`, `board` and `market` are `Field`s: a document that does not carry one sets it to null with the reason `not part of this document; see ...`.

**Recommendation: a separate path, not a new top-level key inside `/api/room`.** Reasons:

- v2 is frozen and additive-only; its `now` is the forming/confirming/active/fading/archived lifecycle, which the companion must not read (Bolo's 00:24 ack). A separate document keeps the two vocabularies from ever sharing a key.
- `room.json` is about 100 MB and rebuilt on request (T2 §2.3). The companion needs one small document per view, rebuilt on change, with its own ETag (T2 §4.5).
- Diffs and the board are per member. They cannot share a cache entry with the public v2 room.
- Retiring v2 later is then deleting a path, not a migration.

`meta` (`Meta`): `contractVersion` (`companion-v3.2`), `generatedAt`, `asOf`, `clock` (`availableAt`), `projectionVersion`, `materialityVersion`, `lightsVersion`, `viewer` (member id, or null with reason `public projection`), `memberView`, `etag`, `v2 { path, readModelVersion, relation }`, `fixture`.

`capabilities` (`Capabilities`): per chain, per light, a `Field` whose value is `true` when the chain can show the light and null with the refusal line otherwise; plus `attentionSources[]` coverage rows. Refusals (§3.4) are computed from this.

### 1.1 Bolo's five distinctions, and where each lives

| | v2 had | v3 companion has | Where |
|---|---|---|---|
| (a) | `now` = token keys ordered by the ladder lifecycle (`state` forming ... archived) | eight lights (store) and a six-word projection (label); situation lifecycle is open, reopen as new, close; `cooling` is a list section | §2 `lights`, `lifecycle`; §3; §7 |
| (b) | `market` from the market cache, `chart.embedUrl` | `market.price`, `changeFromFirstSeen`, `drawdownFromHigh`, `volume1h`, `netFlow1h`, `walletsIn1h` are tape; `market.provider.{marketCap, liquidity, volume24h, chart}` are provider, each with `switchNote` and `replacement`; missing tape numbers are null with a reason, never a provider fallback | §2.3 |
| (c) | `theses[].text`, `thesisText` | no text. A thesis is an event of type `trader_thesis` on the money lane with a stance label (stance counts only); a caller call is `caller_call` with channel in a member block | §0.10, §6 |
| (d) | `meta.providers`, `capabilities` as process health | `coverage[]` per source for this situation (seen, cadence, last, gap) and `pools[]` per pool (tape coverage status over a half-open window, covered cursor, measured head, lag between them, last trade, seek cursor, missing spans). Process health stays on `/status` | §2.5 |
| (e) | none | `pools[].bondingCurve { virtualTokenReserves, virtualQuoteReserves, realTokenReserves, realQuoteReserves, tokenTotalSupply, complete, quoteMint, quoteIsNative, tokenDecimals, quoteDecimals, realQuoteReservesSol, decodedFrom }` following the current Pump IDL; raw u64 decimal strings in raw units; SOL only for a verified native quote; null with a reason until the decode is live | §2.5 |

## 2. `Situation` (`situation.schema.json`)

One per situation: a token plus a clock (T5 §0, §6). Keyed `chain:address#openedAt~k`, where `openedAt` is the `availableAt` of the opening event, to the second, and `k` is the first 8 hex of sha256 of that event's `sourceKey` (§0.14).

| Field | Type | Meaning |
|---|---|---|
| `situationId` | string | `chain:address#openedAt~k` |
| `key` | string | `chain:address` |
| `asOf` | ISO | when this document was computed |
| `identity` | `{chain, address, symbol, name: Field}` | exact address literal |
| `lifecycle` | `{status, openedAt, openedBy[], openingKey, closedAt, closeReason, predecessor, cooling}` | §2.1 |
| `band` | Field | `under_250k`, `250k_1m`, `1m_10m`, `10m_50m`, `over_50m`; `basis` names the cap it came from (provider today, tape after the switch) |
| `age` | `{situationMin, poolAge: Field}` | minutes since open; minutes since the pool's first swap on our tape |
| `market` | `Market` | §2.3 |
| `holders` | object of Fields | `earlyCohortRetention` (pct, rpc), `earlyCohortSize`, `walletsHoldingTape` (overstates; labelled), `top20Share` (of the tape-attributed float), `walletsBeforeReference`, `referenceEvent` |
| `lights[8]` | `Light` | §2.2 |
| `state` | `SituationState` | §3 |
| `relationships[]` | `Relationship` | §5 |
| `diffs[]` | `SituationDiff` | §4; empty in the public projection |
| `coverage[]` | `CoverageRow` | §2.5 |
| `pools[]` | `PoolRow` | §2.5 |
| `events[]` | `Event` | every event referenced anywhere in the document, `availableAt` ascending (§6) |

### 2.1 Lifecycle (T5 §6, not a ladder)

- **open**: the first intake event from any of the six intake sources (watched wallet, Fomo-profiled trader, Fomo thesis, Telegram mention, X mention, caller call). Tape rows alone do not open a situation.
- **close**: every light off for 24 h, or liquidity under $1K, or a rug signature. `closedAt` and `closeReason` are Fields, null with reason `open` while open.
- **reopen**: a new event on a closed token opens a **new** situation with a new id; `predecessor` links the old one so "seen before" works. AGRIPPA shows this: a watched-wallet event opened a situation on 17 Sep; it closed by rule; the caller call on 18 Sep opened the current one.
- `cooling` is true when every light has decayed off. It places the row in the Now list's Cooling section (§7). It is not a state word.

### 2.2 `Light`

Exactly eight, always in T5 order: `wallets, traders, callers, attention, tape, holders, momentum, safety`.

| Field | Meaning |
|---|---|
| `name`, `class` | class used by the projection: `money` (wallets, traders, tape), `attention` (callers, attention), `holders`, `momentum`, `safety` |
| `status` | `lit`, `off`, or `refused` (the chain cannot show it) |
| `intensity` | Field: 1, 2, 3 (safety: `amber` or `red`) per T5 §2.2; null with reason when off or refused |
| `decayed` | Field: `intensity x 0.5^(age / halfLife)`, age from `lastEventAt`; off below 0.25; hard cap 72 h |
| `litAt` | Field: `availableAt` of the first event of the current lit episode; the projection orders on this |
| `lastEventAt` | Field: most recent lighting event (the decay clock) |
| `halfLifeMin` | Field: the band's half-life (T5 §2.2); null for safety (no decay while the condition holds) |
| `summary` | the clause the state sentence uses ("a watched wallet in") |
| `evidence[]` | event ids; never empty when lit (the ladder audit's 47% empty-evidence failure is a schema error here) |
| `refusal` | Field: value `true` when the chain can show the light; null with the refusal line when refused |

### 2.3 `Market`

| Field | Basis | Null reason when missing |
|---|---|---|
| `price` | tape | none expected on taped chains; untaped chains refuse with `Tape: not read on this chain.` |
| `changeFromFirstSeen` | **v3.3:** a `Change` object `{value, basis: since_first_seen, firstSeenAt, firstSeenPrice}`, the same type as the Now row's `market.change` | |
| `drawdownFromHigh` | tape, pct, phantom-filtered | |
| `volume1h`, `netFlow1h` | tape, usd | fixture: `tape volume sum not in the T5 extract`; live: `Tape: not read on this chain.` |
| `walletsIn1h` | tape, count | |
| `provider.marketCap` | **v3.3:** a `MarketCap` object `{value, basis, supply, assumedSupply}`, the same type as the Now row's `market.marketCap`; `basis` `provider:dexscreener` today; replacement tape price x RPC totalSupply | `supply` null with `supply not read; market cap estimated, supply assumed`; `assumedSupply` null with `provider figure; no supply assumed by us` |
| `provider.liquidity` | `provider:dexscreener`; replacement pool reserves by RPC x tape price (curve reserves on pump.fun) | |
| `provider.volume24h` | `provider:dexscreener`; replacement tape volume sum | `less than 24 h of trading`, `provider field awaiting tape` |
| `provider.chart` | `provider:geckoterminal`; replacement none | `embedded provider chart not used by the companion; the clock is drawn from our tape` |

No tape field falls back to a provider value. When the tape cannot produce a number the field is null with a reason; the page prints the reason. That is what lets the Q1 switch happen without a code change on the page.

**v3.2, market cap and change on the Now row** (§7): `market.marketCap = {value, basis, supply, assumedSupply}` with `basis` in `rpc_supply | estimate | provider:dexscreener` (**v3.3:** `estimate` was `assumed_supply`; the reason string `supply not read; market cap estimated, supply assumed` is unchanged), and `market.change = {value, basis: since_first_seen, firstSeenAt, firstSeenPrice}`. `rpc_supply` needs a supply read valid at the moment (§7.2); otherwise `estimate` and the page prints `~`. **v3.3 (Bolo H4):** a supply read is `rpc_supply` only inside its validity interval, and it is total supply, not circulating supply; the page keeps `~` until a circulating basis exists, which v3.3 does not define. **v3.3:** the Situation's `market.provider.marketCap` and `market.changeFromFirstSeen` now use the same two types, named `MarketCap` and `Change` (v3.2 called them `MarketCapV32` and `ChangeV32`; those names are gone). The fixtures carry the same values.

### 2.4 Holder fields

Holder fields come from balances (`rpc`) or, where labelled, the tape-attributed float. The tape overstates holders (T5 §1.8: tape says 1,305 SPRING wallets hold; balances say 750 of 861 early buyers exited), so `walletsHoldingTape` always carries that note and no state or light reads it. On a chain without a holdings pass every holder field is null with `Holders: cannot see on this chain.`

### 2.5 Coverage and pool health

`coverage[]`, one row per source for this situation: `{source, lane, seen, cadenceMin: Field, last: Field, gap: Field}`. This is source coverage (what we saw for this token), not process health (is the collector alive); process health stays on `/status`. The page's compact "data health, N issues" indicator reads `state.dataQuality.issues`; the drawer reads `coverage[]`.

`pools[]`, one row per pool (Bolo item 2; lag and window redefined in v3.1 after Bolo item 4):

| Field | Meaning |
|---|---|
| `pool`, `dex` | exact pool literal, DEX or launchpad |
| `status` | `ok`, `gap`, `not_found`, `archive_unavailable`, `not_taped`: the tape coverage record of T2 §2.3 for the window; without it a missing trade and a quiet minute look the same |
| `window` | `{from, to, bounds: "[from,to)"}`: the displayed window, half-open. `from` inclusive, `to` exclusive (normally `asOf`). Every window in this contract is half-open. |
| `fromBlock`, `toBlock` | Field: first and last block (or slot) covered inside the window |
| `coveredBlock` | Field: the covered cursor, the highest block through which every row for this pool is on our tape; `asOf` on the Field is that block's time |
| `sourceHead` | Field: the measured source head block; `asOf` is its block time, `note` says when it was measured |
| `lagSec` | Field: `sourceHead` block time minus `coveredBlock` block time, seconds, never negative. **Not** the time since the last trade: a quiet pool with a current cursor has a small lag (V40, brief V24). **v3.2:** a lag needs a measured, consistent head: `headMeasuredAt` present (and at or before `asOf`) and the head block at or above the covered block. Otherwise null with `head_unmeasured` (V45, brief V29) or `head_inconsistent` (V46, brief V30). Null with a reason when either end is missing. Computed by `coverage.pool_lag(row, asOf)`. |
| `headMeasuredAt` | **v3.2.** Field: when the source head was measured, or null with `head_unmeasured`. |
| `headAgeSec` | **v3.2.** Field: head freshness against `asOf`, `asOf` minus `headMeasuredAt`, seconds. Freshness is always measured against `asOf`, never against the covered cursor. Null with `head_unmeasured` when not measured or measured after `asOf` (not knowable at `asOf`). Computed by `coverage.head_age`. |
| `lastTradeAt` | Field: the last swap on our tape for this pool, kept apart from `lagSec`; null with `no swap in the window` |
| `seekCursor` | Field: the block where reading resumed after an archive refusal, or null with `no archive refusal`. A seek cursor is never `coveredBlock`: the span before it stays missing. |
| `missingSpans[]` | `{from, to: Field, fromBlock: Field, toBlock: Field, reason}`: historical spans with no coverage, with dates and a reason. They stay listed after current rows resume; a current seek does not heal history. |
| `firstSeenAt` | Field: first swap on our tape for this pool |
| `bondingCurve` | `BondingCurve`, below |

`bondingCurve` (pump.fun, Solana), v3.1 after Bolo item 2. Field names follow the current Pump IDL struct `BondingCurve` in camelCase. Source: the IDL Bolo downloaded from `https://raw.githubusercontent.com/pump-fun/pump-public-docs/main/idl/pump.json` to `projects/crypto-2026-09/signal-room/build/companion-overnight-20260927/pump-idl.json`, sha256 `ffe966c42f1af41652ee753fe2f1e3f7cd4077d7e6f49faf3138959c8b56064b`, program `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`. The schema description cites the same file and hash.

| Field | IDL field | Unit | Encoding |
|---|---|---|---|
| `virtualTokenReserves` | `virtual_token_reserves` | `token_base_units` | u64 decimal string |
| `virtualQuoteReserves` | `virtual_quote_reserves` | `quote_base_units` | u64 decimal string |
| `realTokenReserves` | `real_token_reserves` | `token_base_units` | u64 decimal string; the pre-migration token count (tokens still on the curve), Bolo M2 item 5 |
| `realQuoteReserves` | `real_quote_reserves` | `quote_base_units` | u64 decimal string |
| `tokenTotalSupply` | `token_total_supply` | `token_base_units` | u64 decimal string |
| `complete` | `complete` | `bool` | boolean, required; the authoritative completion flag |
| `quoteMint` | `quote_mint` | `pubkey` | exact base58 literal; null with a reason on legacy layouts that carry none |
| `quoteIsNative` | derived from `quoteMint` | `bool` | true only when `quoteMint` is verified to be the wrapped SOL mint `So11111111111111111111111111111111111111112` |
| `tokenDecimals`, `quoteDecimals` | mint accounts | `count` | verified from the mint accounts; never assumed (the v3 text said 6) |
| `realQuoteReservesSol` | derived | `sol` | exact `realQuoteReserves / 10^9` as a decimal string, **only** when `quoteIsNative` is true; otherwise null with `quote not verified native SOL; no SOL denomination` |
| `decodedFrom` | none | `text` | the IDL file and hash the decode followed |

Every value is a `Field` with `unit` and `basis` required (`rpc`, or `derived` for the SOL figure and `decodedFrom`). A raw value is a canonical u64 decimal string (0 to 18446744073709551615, no sign, no leading zero, no exponent); a JSON number, an object, or any other unit fails the schema (V38, brief V22). `complete: true` does not force any reserve to zero: reserves are what the account holds, and a reserve that was not read is null with a reason, never `"0"`. The schema also refuses a SOL value when the quote is not verified native, and pins `quoteMint` and `quoteDecimals` (9) when it is. `tokensLeftBeforeMigration` is gone: Bolo's answer is that the pre-migration count is `realTokenReserves` as read.

**Adapter check (v3.2, `adapter.admit_curve`).** When `quoteIsNative` is true, `realQuoteReservesSol` must equal `realQuoteReserves / 10^9` exactly, compared as decimals. A schema-valid curve that fails (raw `1000000000`, SOL `999`) is refused with `conversion_mismatch` (V58, Bolo pass12 hardening). JSON Schema does not do arithmetic; this check lives in the adapter, and the validator also runs it over every fixture curve.

On other launchpads every field is null with `not a pump.fun pool`; on pump.fun pools before the decode is live, `pump.fun curve decode not live yet`. Bolo's census has 0 of 161 resolved pools with a live curve, so no fixture carries a decoded curve; V38 carries one as a schema case only.

## 3. `SituationState` (`state.schema.json`)

| Field | Meaning |
|---|---|
| `state` | `developing`, `money_first`, `attention_first`, `crowd_arriving`, `early_money_leaving`, `safety_changed`; **v3.2:** or `unknown` when the input that decides the word is not admissible (§3.7). `unknown` is not a seventh word on the page: the page prints the sentence, and `stateReason` names why |
| `stateReason` | **v3.2.** Present only when `state` is `unknown`: `retention_unavailable` |
| `sentence` | one evidence sentence, present tense, no verdict words, durations not clocks; produced by the rule's template (§3.2) |
| `rule` | the decision-table row that produced it (`R1a` ... `R5c`, or `R2u`, v3.2) |
| `evidence[]` | event ids behind the sentence |
| `since` | `availableAt` of the event that made the current word true |
| `previous` | Field `{state, until}`, or null with `no earlier word in this situation` |
| `waitingFor` | `{sentence, why}`: what would change the read, and why |
| `dataQuality` | `{issues: n, notes[]}`: open gaps, stale reads, blind sources for this room |
| `refusals[]` | `{light, reason, basis, statesNotComputed[]}`; `basis` is `chain_cannot_show` or `support_unreported` (§3.4) |
| `inadmissible[]` | `{input, at, reason}`: evidence the availability gate kept out (§3.7), lights, the safety event and (v3.2) the retention cohort; empty in every fixture |
| `hero` | `{field, reference: Field}`: the hero number and what it is read against, chosen by state (§3.6) |
| `lagReason` | Field: the relationship id the lag bracket draws, or null with the reason there is no bracket (MOSSY: `no lag bracket: only one kind of event`) |
| `projectionVersion` | `p2-provisional` (v3.1: `p1-provisional`, v3: `p0-provisional`; v3.2 changed the rule at the retention gate and the clock parser, not in its numbers) |

**The rule, printed:** the state is a label on the current lights. It has no path, no windows that archive, and no order of words. It is recomputed every tick from the light set and the latest reads, and it never reads the previous word, so any word may follow any word. Windows appear only to measure a trend (retention fall, breadth rise); nothing is archived on silence. Most rows read *Developing*; that is the base rate (T5 §2.1: 83% of situations never get a second independent signal within an hour), not a failure.

### 3.1 Decision table

Evaluated top to bottom; the first row that holds wins. This is the override order: **safety first**, then early money leaving, then crowd arriving, then order, then developing. "Lit" means `status: lit` (decayed value at or above 0.25). Money lights: wallets, traders, tape. Attention lights: callers, attention. Times compared are `litAt` (availableAt).

| Row | State | Condition (all must hold) | Needs on chain |
|---|---|---|---|
| R1a | safety_changed | safety light lit (amber or red) | none |
| R1b | safety_changed | an admitted safety-lane event (availableAt at or before asOf, §3.7) within the last 60 min (types `lp_status`, `lp_event`, `deployer_move`, `liquidity_pull`, `rug_signature`, or a `contract_check` whose result changed); not `suspect_print`, not `gap` | none |
| R2u | unknown (v3.2) | holders capability reported true, an attention light lit, a retention cohort supplied, and the cohort not admissible at `asOf` (its `availableAt` missing, malformed or after `asOf`). Reason `retention_unavailable`. No lower row may claim a word, because the one input that decides R2 is missing | holders |
| R2 | early_money_leaving | holders capability reported true, an attention light lit, the retention cohort admissible (v3.2: `retention.availableAt <= asOf`), and early-cohort retention fell 10 points or more inside the window (window = the band's holders half-life: 6 h under $1M, 12 h $1M to $10M, 24 h above; fall = highest read in the window, including the last read before it, minus the latest read in it) | holders |
| R3 | crowd_arriving | tape capability reported true, a money light and an attention light lit, and over the band's primary flow window: wallets in last window at least 1.5x the window before and at least 20; mentions last window at least 1.5x the window before and at least 3 | tape |
| R4a | money_first | two or more lights lit (safety excluded), a money light lit, no attention light lit | none |
| R4b | attention_first | two or more lights lit, an attention light lit, no money light lit | none |
| R4c | money_first | two or more lit; both classes lit; earliest money `litAt` precedes earliest attention `litAt` by at least one cadence | none |
| R4d | attention_first | as R4c, the other way round | none |
| R4e | developing | both classes lit and their first `litAt` within one attention-source cadence (Telegram preview: 5 min): the order cannot be told | none |
| R5a | developing | no light lit | none |
| R5b | developing | exactly one light lit (safety excluded) | none |
| R5c | developing | two or more lit, none of them money or attention | none |

Tie-breaks: inside R4, equal classes compare the earliest `litAt` of each class; a gap under one cadence is R4e, exactly one cadence or more is ordered. Inside R3 and R2, both conditions true means R2 (V23). A safety light and anything else means R1 (V19, V20).

### 3.2 Sentence templates

| Row | Template |
|---|---|
| R1a | `Safety {amber\|red}: {light summary}, {age} ago.` |
| R1b | `Safety event: {event summary}, {age} ago.` |
| R2u | `State not computed: the early-cohort retention read is not available at this moment.` (v3.2) |
| R2 | `An attention light is on while early-cohort retention fell {n} points in the last {window}, by RPC read.` |
| R3 | `Wallets in rose {a} to {b} and mentions {c} to {d}, last {w} against the {w} before.` |
| R4a | `Money lit first: {money summary}, {age} ago; no attention light on now.` |
| R4b | `Attention lit first: {attention summary}, {age} ago; no money light on now.` |
| R4c | `Money lit first: {money summary}, {age} ago; attention followed {lag} later.` |
| R4d | `Attention lit first: {attention summary}, {age} ago; money followed {lag} later.` |
| R4e | `Money and attention lit within the same {cadence}-minute read; which came first cannot be told.` |
| R5a | `No light is on now.` |
| R5b | `One light on: {summary}, {age} ago.` |
| R5c | `{n} lights on, none of them money or attention: {names}.` |

When the state is *developing*, each refusal line (known or unreported) is appended (`One light on: a tracked wallet in, 7 min ago. Holders: cannot see on this chain.`). Pages may print a longer evidence line from the event list beside it; the contract sentence is the one the rule can prove.

### 3.3 Numbers (provisional, `p0-provisional`, in `projection.py` `PARAMS`)

```
offBelow            0.25    decayed value below this is off (T5 2.2)
hardCapHours        72      any light older than this is off
retentionStepPts    10      early-cohort retention fall that sets R2
crowdRatio          1.5     last window over previous window, wallets and mentions
crowdMinWallets     20      wallets in during the last window, at least
crowdMinMentions    3       mentions during the last window, at least
safetyRecentMin     60      a safety event this recent sets R1b
defaultCadenceMin   5       Telegram public preview cadence (the read 3.2)
```

None of these is fitted (T5 §8). Each change bumps `projectionVersion` and is logged, as `states.json` did.

### 3.4 Refusal rule

A state that needs a light the chain cannot show is not computed. **Only a reported `true` makes a light computable** (v3.1, Bolo item 3). The capability can be in three conditions: reported `true` (compute), reported `false` (known refusal, basis `chain_cannot_show`, the reason `Holders: cannot see on this chain.` or `Tape: not read on this chain.`), or anything else, including absent, null or not a boolean (basis `support_unreported`, the reason `Holders: source support not reported; not computed.` or `Tape: source support not reported; not computed.`). Unknown source support never opts into computability: v3's `projection.py` defaulted a missing capability to true at lines 88, 95, 114 and 125, so retention values alone produced *Early money leaving* with no holders support on record. V39 (brief V23) is that probe; it now reads *Developing* with both reasons. The known-false rule is unchanged. R2 needs holders (Robinhood Chain only today); R3 needs our tape (not Base or Ethereum). For every refused light, `refusals[]` carries the light, the line the page prints (§10), and the states skipped. The other rows still run: a Solana situation with two money lights still reads *Money first* (V17 reads *Attention first* with retention numbers present and ignored). When nothing else holds the row reads *Developing* with the refusal line appended (MOSSY, V18).

### 3.5 Data quality

`dataQuality.issues` counts open items for this room: every coverage row with `seen: false` or a `gap` value, every gap event inside the displayed window, every stale read past twice its cadence, every suspect print in the window. `notes[]` has one printed line per issue. Suspect prints and gaps never change the word (§12 item 5). Inadmissible evidence (§3.7) counts as an issue.

### 3.6 Hero (v3.1)

T7 §3.2: the state chooses the hero number, never habit. `state.hero = {field, reference}` names both by path, relative to the situation. Default table (build-0's `heroFor`, adopted):

| State | `field` | `reference` |
|---|---|---|
| money_first | `holders.walletsBeforeReference` | `market.walletsIn1h` |
| crowd_arriving | `holders.walletsBeforeReference` | `market.walletsIn1h` |
| early_money_leaving | `holders.earlyCohortRetention` | `diffs[i].checkpoint.snapshot.measures.earlyCohortRetention` for the member's checkpoint that stored it; null with `no checkpoint measure for this member` otherwise |
| attention_first | `market.walletsIn1h` | null, `no reference for this state` |
| safety_changed | `state.evidence[0]` (the safety event) | null, `no reference for this state` |
| developing | `lights[status=lit].count` (lights on, derived) | `lights[status!=refused].count` (lights this chain can show) |

A hero whose Field is null prints its reason, like any other Field.

### 3.7 Availability gate (v3.1)

The projection admits evidence only when `availableAt <= asOf` (age at or above zero), compared to the second (§0.12). A lit light whose `litAt` is after `asOf` or missing is treated as off; a safety event dated after `asOf` or without a clock is dropped. Each rejection goes to `inadmissible[]` with `evidence available after asOf; not admitted` or `evidence carries no availableAt; not admitted`. No sentence is ever built from a negative age: R1b additionally requires the age to be between 0 and 60 min. v3's `projection.py:107` checked only `age <= 60 min`, so a 2099 event against a 2026 `asOf` printed `Safety event: synthetic future event, -38006640 min ago.` V37 (brief V21) is Bolo's probe; it now reads *Developing* (R5a) with one inadmissible note.

**v3.2, the retention gate (Bolo pass12 item 1).** The v3.1 gate covered lights and the safety event, not retention: with holders true, retention 90 to 20 and `retention.availableAt` 2099, v3.1 printed *Early money leaving*. A retention cohort is admissible only when `retention.availableAt <= asOf` and `capabilities.holders` is reported true. When holders is true, an attention light is lit and a cohort is supplied but not admissible, the state is `unknown` with reason `retention_unavailable` (R2u), and the cohort is listed in `inadmissible[]`. V42 (brief V26) is Bolo's probe. When holders is not reported true, the v3.1 refusal stands (V39 is unchanged): the brief's "otherwise unknown" is read as applying to the holders-true case, because the known and unreported refusals already say more than `unknown` would. Safety (R1) is still evaluated first.

**v3.2, malformed clocks.** Every clock goes through `clocks.parse` (§0.15). v3.1 raised `TypeError` on an offset-naive `availableAt` and normalised `+00:60`; both are now refused with a reason and listed in `inadmissible[]` (V55 to V57).

**v3.3, clock order.** A safety event or retention cohort that carries both a source clock `at` and `availableAt` goes through `clocks.admit_pair` (§0.17): `at` after `availableAt` is listed in `inadmissible[]` with `evidence clock after its availability; not admitted`. Inputs with one clock are gated as in v3.2.

## 4. `SituationDiff` (`diff.schema.json`)

From a checkpoint to now. Checkpoints are user-lane events: `viewed` (the last look), `watched`, `entered` (the "in" tap).

| Field | Meaning |
|---|---|
| `checkpoint` | `{kind, at, eventId, snapshot}`; `snapshot` = `{state, lights[{name, status}], measures{...Fields}}` stored at the checkpoint |
| `asOf` | now |
| `changed[]` | `DiffLine`s that changed |
| `stillIntact[]` | lines confirmed unchanged by a fresh read (each points at the read that confirms it) |
| `unknown[]` | lines we cannot confirm: a gap, a stale read, a blind source (each points at the gap event) |
| `sinceYouLooked[]` | at most five event ids into `changed[]` and `unknown[]` |
| `materialCount` | material lines in `changed[]` |
| `materialityVersion` | `m0-provisional` |
| `materialityRule` | the §4.2 rule in words, printed under the list beside `materialityVersion` (v3.1) |

`DiffLine`: `{field, before: Field, after: Field, eventId, at, material, materialBecause: Field, sentence}`. `eventId` is the event that changed, confirmed or degraded the field; for a light that decayed off it is the last lighting event, and `at` is the computed off time.

### 4.1 Entry snapshot

When a member taps "in", M2 stores the `SituationState` word and the light set at that instant, plus the measures the hold view compares (price, retention, top-20 share, liquidity). "Still intact" is a diff against that snapshot. Without it "still intact" cannot be computed (the read §2.3).

### 4.2 Materiality (provisional, printed on the page under the list)

```
materialityVersion   m0-provisional
lightFirstEvent      true   a light's first event (off to lit, including a relight after decay)
lightOff             true   a light that was lit at the checkpoint is off now
safetyEvent          true   any safety-lane event except suspect_print and gap
stanceFlip           true   a caller stance change on this token
retentionStepPts     10     early-cohort retention changed 10 points or more between checkpoint and now
stateChange          true   the state word differs from the checkpoint's (added by this contract, see 12.6)
price moves          never material
single mentions      never material
```

### 4.3 Since you looked

Sort: material lines first, then recency (`at` descending). One entry per event id. At most five. The page prints the materiality block under the list, marked provisional. "No material change since HH:MM" is a valid Board row (§8).

**v3.2, display strings (fixed).** Checkpoint kinds stay `viewed | watched | entered`; the display words are `look` and `entry`. The Now row's For you (§7) prints exactly one of five forms, computed by `nowat.for_you_at` and `for_you_text`:

| `forYou.kind` | Headline | Small lines |
|---|---|---|
| `since_look` | `N material since look · HH:MMZ` | |
| `since_entry` | `In · N material since entry · HH:MMZ` | `N since look` when there is a look |
| `new_to_you` | `New to you` | |
| `no_material_change` | `No material change · since look HH:MMZ` | |
| `before_look` | `Before your look` | `looked HH:MMZ` |

An entry at or before the moment wins; else no look ever is `New to you`; else a moment before the look is `Before your look`; else the count since the last look. Material counts use events after the checkpoint with `availableAt` at or before the moment.

## 5. `Relationship` (`relationship.schema.json`)

Lead-lag between the first light of one class and the first light of the other. The T5 attention-to-money lag becomes this object.

| Field | Meaning |
|---|---|
| `id`, `kind` | `rel-...`, `attention_money_lag` |
| `from` | `{lane, eventId, at}`: the earlier event |
| `to` | Field: `{lane, eventId, at}`, or null with `no attention event seen yet` (or `no money event seen yet`) while open |
| `durationMin` | Field: closed length, or null with `open` |
| `elapsedMin` | minutes from `from` to `asOf` (the open bracket's running length) |
| `open` | true while the other class has not arrived |
| `source`, `cadenceMin` | the attention source that bounds the measurement and its cadence (the quantisation of the lag) |
| `caption` | printed beside the bracket until the source changes |
| `sentence` | `Money led attention by {n} min, and counting.` / `Attention led money by {n} min.` |

**Caption rule.** Source and cadence print beside every bracket ("Telegram preview, 5 min") until the source changes (X back, MTProto, a second preview cadence). The caption is data, not page copy, so the change is one field. A lag shorter than one cadence is never printed as an order (R4e).

## 6. Event grammar

Every event: `{id, lane, type, providerAt: Field, availableAt, receivedAt, source, sourceKey, sourceId: Field, sha256: Field, quality, glyph, summary, until: Field, values{...Fields}, lights[], member?}`. `quality` is T2's set plus `gap` and `illustrative`. `glyph` names a glyph family for the clock (T7 C05); colour is never in the contract.

| Lane | Types | Glyph families |
|---|---|---|
| money | `wallet_in`, `wallet_reduced`, `wallet_out`, `trader_in`, `trader_reduced`, `trader_out`, `trader_thesis`, `tape_read`, `tape_surge`, `tape_anomaly`, `flow_turn`, `momentum_high`, `band_move`, `gap` | dot (watched wallet), diamond (profiled trader, thesis), ring (reduced or out), tick (tape read, surge, anomaly), step-down (flow turn), step-up (new high), double-rule (band move), hatch (gap) |
| holders | `retention_read`, `retention_step`, `concentration_read`, `recurring_cluster`, `gap` | tick, step-down, dot, hatch |
| attention | `caller_call`, `caller_stance`, `telegram_mention`, `x_mention`, `sweep_empty`, `gap` | triangle (call, mention), triangle-open-down (stance change), none (empty sweep), hatch |
| safety | `contract_check`, `lp_status`, `lp_event`, `deployer_move`, `liquidity_pull`, `rug_signature`, `suspect_print`, `gap` | check-box, square (LP), step-down, strike (suspect print), hatch |
| user | `viewed`, `watched`, `unwatched`, `entered`, `exited`, `alert_sent` | pin |

**Provider print flags** (for C15; v3.1 after Bolo item 5, code in `coverage.print_flags`). Four separate flags, each over its own denominator. None of them is inferred from another, and nothing is removed on a repeat rate.

| Flag | True when | Denominator |
|---|---|---|
| `unchanged` | the price equals the previous comparable persisted observation for the same pool and provider | rows with a previous comparable observation |
| `providerStale` | the provider clock did not advance from the previous observation | rows where both provider clocks exist |
| `phantom` | a distinct observation (a repeat is judged once, with the observation it repeats) under 0.75x of **both** distinct neighbours, **and** our tape is covered over the **whole** support window `[candidateAt - 60 s, candidateAt + 60 s)` (v3.2: the union of covered half-open intervals must contain it; a covered instant is not enough) with no swap at that price (within 5%) in that window. Values: `true`, `false`, `unverified` (v3.2: any distinct observation with both neighbours whose support window is not covered, candidate or not), or null (no two neighbours) | distinct observations with both neighbours and a covered support window; `unverified` rows are counted apart and excluded (v3.2 implements this; v3.1 let an uncovered non-candidate in as `false`) |
| `misquote` | orientation: the print sits at 1/tape price rather than the tape price, against a covered tape swap within 60 s | rows with that tape reference |

Only `phantom: true` removes a print from ranges and drawdowns. An `unverified` print stays in, marked, and counts in `dataQuality`: with incomplete tape, "no swap at that price" cannot establish a phantom. V41 (brief V25): a low print the provider repeats, with no covered tape, is `unverified` in both rows and nothing is removed. **v3.2:** V43 (brief V27): a two-second covered interval around a 12:01 candidate (prices 1, 0.4, 1.02) is `unverified`, not `phantom`, and nothing is removed. V44 (brief V28): prices 1, 1.01, 1.02 with no covered tape give `unverified` and a phantom denominator of 0. The documented denominator stands; v3.2 implements it. A `suspect_print` event carries the flags in `values` with the print under `basis: provider:...`, counts in `dataQuality`, and never sets a light or a word.

**Gaps** (for C14): a `gap` event in the lane it blinds, `providerAt` null (`source gives no clock of its own`), `until` set when the gap closed or null with `gap still open`. Examples in the fixtures: the Telegram preview missing an hour of messages, X not read since 18 Sep, balance reads not run this hour, Holders: cannot see on this chain.

**What never appears in an event:** caller or thesis text, caller names outside `member`, instructions.

Lights and lanes: an event lights at most the lights named in its `lights[]`. A light's class comes from the light, not the event's lane.

**A `trader_thesis` is a money-lane event** (v3.1). It re-lights `traders`, a money light (T5 §2.2), and it does **not** close an open attention bracket: the attention-money relationship (§5) closes only on the first attention light. v3 put the thesis on the attention lane, so SPRING drew a thesis diamond on the attention lane inside a bracket that still said "no attention event seen yet" (build-0 CHECKS §2). The fixtures now draw it on the money lane (SPRING `ev-sp-f2207`, AGRIPPA `ev-ag-f4418`, and SPRING's mini clock).

## 7. `now` (`now.schema.json`), v3.2, amended v3.3: the frozen row

**v3.2.** Rewritten after the design freeze (DESIGN-FREEZE §2, row "Contract §7 `NowRow`"; pass 7 NOTES (b) is the page). The v3.1 six-element row (headline, whyNow, miniClock in minutes, waitingFor, changes) is kept only as `now.sections` (`NowRowV31`) so build-0 renders until its page change, and is removed at admission. Code: `nowat.py`. Schema: `Now`, `NowRow` and the definitions below.

`now = {asOf, window: {start, end, bounds}, history: {since, kind}, rows[], orderRule, endLine}`.

- `asOf`: to the second (§0.12). `window`: the displayed window, half-open `[start, end)`, `end = asOf` (the 60-minute rail).
- `history` (**v3.3**, was `historyFrom`): `since` is the clock versioned per-tick history began for the store; `kind` is `persisted` (per-tick rows exist from `since` onward) or `prospective` (the clock a staged store *began* persisting, as in Bolo's B2 staging receipt). It is a property of the store, not of a row.
- `rows[]`: the frozen rows, in order at `asOf`. `orderRule`: the order in words (printed in About this data). `endLine`: `No other situations meet the surfacing rules at HH:MMZ.`

### 7.1 The rule: Now at `t`

**Now at `t` is the rows with `surfacedAt.value <= t`, every field `last(t)`**: the last admitted observation (**v3.3:** admitted means it passed its admission check, §0.17 and §0.18, not only that it is available) whose `availableAt` is at or before `t`. No interpolation, no future value. Rows that had not surfaced at `t` are absent (V51, brief V35); a read available after `t` is absent at `t` even when its block time is earlier (V52, brief V36). A request for `t < history.since` refuses with `history not kept before <clock>` (§10; V47, brief V31); nothing is reconstructed from legacy transition rows, and no historical `surfacedAt` is derived from first source-event time (Bolo 20:30). **v3.3:** the same refusal applies to a `prospective` store, and a `prospective` store never yields an original `surfacedAt` for anything that surfaced before `since`. A row whose `surfacedAt.value` is null (`original surface clock not captured`) is not on Now at any `t`; `now_at` lists it under `refusals[]` with that reason (V65). A request for `t > asOf` refuses with `moment after asOf; not known yet`. History must be persisted before a production rewind claims it: this is an acceptance criterion of v3.2, not only a dependency.

The member's own checkpoints are the one exception to `last(t)`: For you compares `t` with the member's looks and entry as recorded at `asOf` (the viewer's record, not market evidence), which is how `Before your look` can be said at a moment before the look.

**v3.4, the page minute.** The rail is the 60 minutes before `asOf`. Minute `m` of the rail (0 to 60) is the moment `asOf - (60 - m) minutes`, with the seconds (and any fraction) inherited from `asOf` (`nowat.rail_moment`). On the fixture, `asOf` is 17:27:30Z, so minute 30 is 16:57:30Z, not 16:57:00Z: SPRING's 16:57:00Z read, available at 16:57:06Z, is on the page at that minute, and the pass 7 strings at 16:57, 16:42 and 16:32Z are the strings at 16:57:30, 16:42:30 and 16:32:30Z. A rail minute is labelled `HH:MMZ` from its own clock; an exact `?t=` clock is used as given and labelled `HH:MM:SSZ` (`nowat.moment_label`). v3.3 said a page minute `HH:MM` was `HH:MM:00Z`; the validator and build-1 never used that reading, and under it every SPRING read would appear a minute late (V70; §12.2 item 17).

### 7.2 The row (`NowRow`)

| Field | Shape | Rule |
|---|---|---|
| `situationId` | Field | the Situation id, or null with `no Situation page for this row` |
| `identity` | `{symbol, name: Field, chain, pool, address, firstSeenAt: Field, trackedFor: Field, following}` | `firstSeenAt` is the first tape price read ever; `trackedFor` minutes since tracking began at `asOf`; `following` prints `Following` |
| `surfacedAt` | Field (**v3.3**, was ISO) | when the row surfaced; Now at `t` holds only rows with `surfacedAt.value <= t`; null with `original surface clock not captured` keeps the row off Now at every `t` |
| `market.marketCap` | `MarketCap` `{value, basis, supply, assumedSupply}` | `basis` in `rpc_supply`, `estimate` (**v3.3** name, was `assumed_supply`), `provider:dexscreener`. **`rpc_supply` only when a supply read `{amount, decimals, block, observedAt, availableAt, validFrom, validTo}` with `basis: rpc` exists, `availableAt <= t` and `validFrom <= t < validTo`.** Otherwise `estimate`, `supply` null with `supply not read; market cap estimated, supply assumed`, and the page prints `~`. No back-projection: a supply read on day 3 does not clear day 1 (V48, brief V32). The prototype Boolean `supplyRead` is not in the contract |
| `market.change` | `Change` `{value, basis: since_first_seen, firstSeenAt, firstSeenPrice}` | the last read with a USD value at `t` against the token's first tape read ever. The baseline is stable when it scrolls out of `window` (V49, brief V33); `window.start` and `firstSeenAt` are separate fields. One read: null with `one read on our tape; no change yet`. No read: null with `not on our tape yet` (the page prints `MC not read`). **v3.3:** reads but none with a USD value at `t` (or the first-seen read's USD not yet available): null with `no USD price at this moment` |
| `market.supplyReads[]` | `Supply` | every RPC supply read, `availableAt` ascending; empty when none was read |
| `clock.priceReads[]` | **v3.3:** `{at, availableAt, pool, basis: tape, source, native: {asset, amount, reason}, usd: {value, basis, conversion, reason}}` | one entry per tape read, no interpolation. Reads with `availableAt > t` are excluded at `t`. `basis` is the constant `tape` and `source` must be `tape:<chain>`, so a provider tick can never be a price read (schema and `nowat.admit_price_read`; V53, brief V37). **v3.3, the B2 seam:** `native.amount` is the full-precision decimal string of the tape quote and `native.asset` the quote asset with its chain (`ETH@rh`, `SOL@sol`, `USDC@base`). `usd.basis` is `tape_usd` (the pool quotes a USD stable; `native.amount` equals `usd.value`), `native_x_provider` (`usd.value` = `native.amount` x `conversion.quote`, exactly, else `conversion_mismatch`), or `unavailable` (`usd.value` null with `usd conversion not available at this moment; native quote retained`). `usd.conversion` is `{source: provider:<name>, quote, at, availableAt}` when `native_x_provider`, else null, and goes through `admit_pair`. A `native_x_provider` value counts at `t` only when `conversion.availableAt <= t`; before that the read is present with no USD value. A provider-converted value relabelled `tape_usd` is refused with `usd basis misdeclared; not admitted` (a non-stable native asset, or a conversion record, contradicts it; V63). `native.amount` may be null only with a reason (the fixtures, which predate the seam: `native quote not in the T5 extract`) |
| `clock.gaps[]` | `{from, to, reason}` | spans with no coverage inside the window, e.g. `not on our tape yet` left of `tapeStart` (the strip hatches it). **v3.3:** at `t`, every read without a USD value at `t` adds a gap from its clock to the next read (or `t`) with `usd conversion unavailable` (`nowat.gaps_at`; V64). Every USD-derived field (price line, `marketCap`, `change`, the readout price, Board's H1 entry cap: **v3.4** `BoardRow.entryCap`, §8) uses only reads with a USD value at `t` |
| `clock.tapeStart`, `clock.window` | Field, window | where the hatch ends; the shared rail window |
| `clock.marks[]` | `{at, availableAt, glyph, width?, title}` | Mini Clock glyphs: `participant`, `attention`, `holders_weakening` (width = burst), `safety` |
| `clock.shareReads[]` | `{at, availableAt, top20Share, basis: tape}` | top-20 share per tick (readout, and the top-20 materiality anchor) |
| `clock.readout` | `{participants, attention, holders, safety}` Fields | with the last price read and `wallets`, the readout `<price> · <entered> · <participants> · <attention> · <holders> · <safety>` |
| `state` | `{word, descriptor: Field, sentence, whyHere, since: Field, ruleVersion}` | at `asOf`; equal to the last `stateHistory` entry. **v3.3:** `since` is a Field; with an empty `stateHistory` it is null with `state history begins at <history.since>` |
| `stateHistory[]` | the same plus `at`, `availableAt` | one entry per tick where the word or its wording changed, immutable, never overwritten. The state at `t` is the last admitted entry with `availableAt <= t`. The old Forming/Active transitions are not this type and are never converted into it. **v3.3:** may be empty (a prospective store has no history before `history.since`). Then the state at `asOf` is the §3 projection's word from evidence at `asOf`, stored in `state` with `since` null; at `t < asOf` the state is null with `state history begins at <history.since>` (V65) |
| `latest` | Field of `MaterialEvent` | the newest material event at or before `t` that changed the row's wording (`changedWording: true`), not any fresh print; null with `no material event that changed wording yet` |
| `materialEvents[]` | `{kind, at, availableAt, anchor: {kind, at}, threshold, line, changedWording}` | §7.3 |
| `wallets` | `{intervals[]: {start, end, availableAt, distinctFirstBuys}, cumulativeDistinctEntrants: Field, latestEntry: Field}` | §7.4 |
| `checkpoints[]` | `{kind: viewed \| watched \| entered, at, availableAt}` | the member's checkpoints (member only) |
| `forYou` | `{kind, checkpoint: Field {kind: look \| entry, at}, materialSince: Field, materialSinceLook: Field}` | the five fixed display strings of §4.3 |
| `order` | `{bucket, reason}` | §7.5 |
| `waitingFor` | string, optional | carried from the Situation; not rendered on Now |
| `grade` | `A`, `B`, `C` | evidence grade of the row's values; the prototype's example rows are `C` and `illustrative: true` |

### 7.3 Material events

`kind` is one of the six printed categories. Every threshold names its anchor (Bolo item-5 pin):

| `kind` | Threshold | `anchor.kind`, `anchor.at` |
|---|---|---|
| `first` | a first of something: first read, first profiled trader, first public mention | `none`, the event's own clock |
| `price_move` | 25% or more | **v3.3 (23:35 pin):** `previous_material`: the price at the row's previous material event of any kind (strictly earlier clock; the price at a clock is the last USD read at or before it); with none, `first_seen`: `firstSeenAt`. v3.2's `previous_same_kind` is gone (V62) |
| `entries` | 50 or more wallets entered in the interval | `previous_interval`: the end of the previous interval (the count is the interval's own distinct first buys) |
| `top20_share` | a move of 0.05 or more | `previous_tick`: the previous share read |
| `safety_change` | any safety change | `none` |
| `state_change` | any change of the state word | `none` |

An event without an anchor, or with an unknown kind or a malformed clock, is refused at the adapter (`material event names no anchor; not admitted`) and by the `MaterialEvent` schema (V54, brief V38). **v3.3:** an event whose `at` is after its `availableAt` is refused with `evidence clock after its availability; not admitted`, and one whose `anchor.at` is after its `at` with `anchor after the event; not admitted` (§0.17; V60). A refused event is never Latest and never counts for For you or the order. On SPRING the pin changes no material event, no line, no Latest and no count; the 17:27Z price move is now anchored on the 17:08:05Z first profiled trader, whose price is the 16:57Z read, so the move is the same +58%. Everything else is an observation (a mark on the clock), not a material event. When several events share one `availableAt`, the one that changed the wording is the highest in the order first, state change, safety change, entries, top-20 share, price move.

### 7.4 Wallets entered

The entrant universe is the distinct wallets with a first buy on the pool since tape start. A wallet's second buy is not a second entry. `cumulativeDistinctEntrants` is the size of that set; per-interval bars are first buys whose `availableAt` falls in the interval, completed intervals only (`nowat.wallets_from_buys`; V50, brief V34). Partial source coverage stays partial, never zero.

**v3.3, the as-of rule (Bolo B6 item 1).** A wallet's entry is its **`observedFirstBuy`**: among that wallet's buys admitted at `t`, the one with the earliest `availableAt` (ties: earliest source clock, then input order). Its entry interval is the interval containing that `availableAt`. Once an entrant has been observed in an interval at some `t`, that interval's count is the same at every later `t`: a buy admitted later has a later `availableAt`, so it can never replace the observed entry. The wallet's **`earliestChainBuy`** (the buy with the earliest source clock) is evidence only: it is retained with its own `at` and `availableAt`, it changes no past count, and it may only be printed as a provenance note (`lateEvidence: true` when it is earlier than the observed entry), never as a move of the entrant. v3.2 sorted by source clock first, so a buy at source 11:00 available 12:10 erased the 12:00 entry of the same wallet from `[12:00, 12:05)` (V59, Bolo's exact case).

### 7.5 Order

Four buckets, printed, no score kept or shown: **1** a safety change in the last 60 min; **2** past Developing with a material event in the last 30 min; **3** other rows past Developing; **4** Developing (and `unknown`). Inside a bucket the newest Latest comes first; ties by symbol. While the pointer or focus is in the list during playback, order and additions hold and values update in place (page behaviour, pass 7).

### 7.6 Fixture

`fixtures/now.json`: `asOf` 2026-09-14 17:27:30Z, `window` [16:27:30Z, 17:27:30Z), `history` `{since: 16:27:00Z, kind: persisted}` (SPRING's tape start: the earliest clock the fixture holds; nothing earlier is invented). SPRING is grade A from the reads already in the fixture: prices 0.00142 / 0.00106 / 0.00168 / 0.00266 at 16:32 / 16:42 / 16:57 / 17:27Z (full precision, each with its own `availableAt`), `firstSeenAt` 16:32Z, look 16:57Z, entry 17:11Z, `marketCap.basis: estimate` with `assumedSupply` 1e9, wallets 638 / 121 / 102 / 78 from the T5 distinct-buyer counts, no supply read, no latest-entry clock. The validator recomputes Now at 17:27, 16:57, 16:42 and 16:32Z from the fields and compares the strings with pass 7 NOTES (c). AGRIPPA, LUMEN, MARLIN, KESTREL and OBOL are the prototype's example rows, grade C, `illustrative: true`, transcribed from `design-pass7-2026-09-27/d2-tracks/now.html`. **v3.3:** the fixture predates the native-quote seam. T5 carries `priceUsd` only, so every read has `native.amount` null with `native quote not in the T5 extract` and `usd.basis: tape_usd` with the T5 value, as v3.2 did; no conversion record is invented. SPRING's pool is ETH-quoted (Bolo B2), so a live SPRING read is `native_x_provider` or `unavailable`, never `tape_usd`.

## 8. `board`

`{asOf, rows[], limits{watching, in}, alertsNote}`. A row (`BoardRow`): `{situationId, symbol, tab: in | watching | changed | archived, checkpoint, state, materialCount, topChanges[max 2], intactLine, frozenRoom: Field, entryCap?}`. `topChanges` are the first two material lines in since-you-looked order. `materialCount: 0` renders "No material change since HH:MM" from `checkpoint.at`. Archived rows carry `frozenRoom` (the frozen room path); open rows have it null with `room is open`. One situation can appear under In and Changed at once. Notifications fire on state changes and material diffs only (G7, wave 1).

**v3.4, the entry cap (`BoardRow.entryCap`, `EntryCap`; code `board.py` `entry_cap_at`; V71).** `{value: PriceField, basis: rpc_supply | estimate, entryAt, read: {at, availableAt} | null, note?}`, computed at `t` from the row's Now evidence and the member's entry clock `entryAt`:
- The entry read is the last admitted tape read with `at <= entryAt` and `availableAt <= t` (`last(t)`, nothing interpolated). None: `value` null with `no tape read at or before the entry`, `read` null.
- Its USD value counts only when a conversion observation valid at the entry clock exists at `t`: `usd.basis: tape_usd`, or `native_x_provider` whose `conversion.at <= entryAt` and `conversion.availableAt <= t`. Otherwise `value` null with `no USD conversion valid at the entry clock` (an `unavailable` USD, a conversion observed after the entry, or one not yet available at `t`; the last becomes a value from the conversion's `availableAt` on).
- Cap = that USD value x supply: `rpc_supply` when a supply read valid at `entryAt` (§7.2) is available at `t`, else `estimate` with the row's assumed supply; the page prints `~` unless `rpc_supply`.

The field is optional in the schema and **admitted for `board.json` when Board gets its v3.3 read model**. Tonight it is only in `staging/board.json` (`python3 contract/board.py --staging`): SPRING, entry 17:11:01Z, entry read 16:57:00Z, `~$1.68M` estimate at 17:27:30Z. `fixtures/board.json` is unchanged and carries no `entryCap`.

## 9. `market`

`{asOf, lines[8], weather[3]}`, T3's first eight in order: `runners_today`, `band_heat`, `breadth`, `chain_heat`, `launchpad_pace`, `dollars_in_out`, `risk_appetite`, `leverage`. A line: `{id, title, text, value: Field, source, cadence, attribution}`. `attribution` is the string printed with the line; its wording follows the rights pack (`rights/ATTRIBUTION-MARKS.md`), which governs over the placeholders in the fixture. `weather` holds the three phrases the Now page's strip shows. Lines describe; none forecasts.

## 10. Null reasons: the catalogue

Every reason string the pages print verbatim. A fixture reason that is not in this list fails `validate.py`. Live reasons may add variables (dates, counts) in the same wording.

**Refusals (chain cannot show a light)**
- `Holders: cannot see on this chain.`
- `Tape: not read on this chain.`
- `Wallets: not watched on this chain.`
- `Safety: no checks on this chain yet.`

**Refusals (source support not reported, v3.1)**
- `Holders: source support not reported; not computed.`
- `Tape: source support not reported; not computed.`

**Availability gate (v3.1)**
- `evidence available after asOf; not admitted`
- `evidence carries no availableAt; not admitted`

**Availability gate and clocks (v3.2)**
- `retention_unavailable`
- `evidence clock has no UTC offset; not admitted`
- `evidence clock offset is not a valid UTC offset; not admitted`

**Adapter (v3.2)**
- `conversion_mismatch`
- `provider tick is not a tape read; not admitted`
- `material event names no anchor; not admitted`

**Clock order and price boundary (v3.3)**
- `evidence clock after its availability; not admitted`
- `anchor after the event; not admitted`
- `price is not a finite positive decimal; not admitted`
- `price read has fields outside the contract; not admitted`
- `reserve is not a canonical decimal; not admitted`

**Native quote and USD (v3.3)**
- `usd basis misdeclared; not admitted`
- `usd conversion not available at this moment; native quote retained`
- `usd conversion unavailable` (a gap at `t`)
- `no USD price at this moment` (page string for this null: G2)

**The v3.2 names, refused (v3.4, §0.19)**
- `key historyFrom is a v3.2 name, not in the contract (history.since); not admitted`
- `value assumed_supply is a v3.2 name, not in the contract (estimate); not admitted`
- `value previous_same_kind is a v3.2 name, not in the contract (previous_material); not admitted`
- `threshold '>= 25% against the previous price move' is v3.2 wording, not in the contract ('>= 25% against the price at the previous material event'); not admitted`
- `key price is a v3.2 name, not in the contract (native.amount, usd.value); not admitted`

**Board entry cap (v3.4, §8)**
- `no tape read at or before the entry`
- `no USD conversion valid at the entry clock`

**Absent surface and state evidence (v3.3)**
- `original surface clock not captured`
- `state history begins at <clock>` (live: the store's `history.since`)

**Now (v3.2)**
- `history not kept before <clock>` (live: the clock, e.g. `history not kept before 2026-09-14T16:27:00Z`)
- `moment after asOf; not known yet`
- `supply not read; market cap estimated, supply assumed`
- `not on our tape yet`
- `one read on our tape; no change yet`
- `no material event that changed wording yet`
- `no descriptor for this word`
- `no Situation page for this row`
- `not an entry row`
- `moment before your look`
- `holders not read`
- `safety not read`

**Sources dark or not running**
- `X not read since 18 Sep`
- `X sweep not running before 16 Sep`
- `Telegram sweep not running before 16 Sep`
- `Telegram and X sweeps not running before 16 Sep`
- `Telegram preview missed messages for an hour`
- `balance reads not run this hour`
- `gap still open`
- `no gap`

**Lights off (not an error)**
- `decayed off`
- `no caller call seen`
- `no mention seen`
- `no Fomo-profiled trader in`
- `no watched-wallet event in this situation`
- `under the band's p90`
- `no new 1 h high`
- `no safety condition`
- `no decay while the condition holds`

**Market and provider**
- `provider field awaiting tape`
- `less than 24 h of trading`
- `embedded provider chart not used by the companion; the clock is drawn from our tape`
- `not a pump.fun pool`
- `pump.fun curve decode not live yet`
- `no suspect print`
- `quote not verified native SOL; no SOL denomination`
- `quote identity unverified`
- `legacy curve layout carries no quote_mint; quote identity unverified`

**Pool coverage (v3.1)**
- `no archive refusal`
- `no swap in the window`
- `source head not measured`
- `no covered cursor for this pool`
- `no covered cursor: reading resumed at a seek cursor after an archive refusal`
- `covered cursor ahead of the measured head; head measurement is stale` (v3.1; v3.2 reports a contradictory head as `head_inconsistent`)
- `head_unmeasured` (v3.2)
- `head_inconsistent` (v3.2)

**Situation and relationship**
- `open`
- `no earlier situation on this token`
- `no earlier word in this situation`
- `no attention event seen yet`
- `no lag bracket: only one kind of event`
- `room is open`
- `token name not read yet`
- `no reference for this state`
- `no checkpoint measure for this member`

**Member and document**
- `public projection`
- `you have not looked at this room`
- `no change since your last look`
- `not material`
- `not part of this document; see fixtures/now.json`
- `not part of this document; see fixtures/board.json` (live: `not part of this document; see /api/companion/board`)

**Event provenance**
- `point event, no interval`
- `source gives no clock of its own`
- `raw sidecar not written for this row (bead 44ht)`

**Fixture-only (never in a live payload)**
- `illustrative event`
- `not carried in the T5 extract`
- `block range not in the T5 extract`
- `slot range not in the fixture`
- `token name not in the T5 extract`
- `tape volume sum not in the T5 extract`
- `tape flow sum not in the T5 extract`
- `hourly count not in the T5 extract`
- `high since open not in the T5 extract (tape reads at 5, 15, 30, 60 min only)`
- `latest entry time not in the T5 extract` (v3.2)
- `native quote not in the T5 extract` (v3.3)
- `first tape read not in the fixture` (v3.3, MOSSY's illustrative change baseline)
- `provider figure; no supply assumed by us` (v3.3, the Situation's provider market cap)

## 11. Versioning and ETags

- `contractVersion: companion-v3.3`. **v3.4 keeps it:** v3.4 changes no required document field and no document on disk; it refuses the v3.2 names (§0.19), defines the page minute (§7.1) and adds the optional `BoardRow.entryCap` (§8). The schema files' `$id` moves to `.../contract/v3.4/...`, so their hashes change; a store pinned to `now.schema.json` re-pins to the v3.4 file. v3.3 renames `now.historyFrom` to `now.history {since, kind}`, `assumed_supply` to `estimate`, `previous_same_kind` to `previous_material` (a rule change, §7.3), `MarketCapV32` and `ChangeV32` to `MarketCap` and `Change`, makes `surfacedAt` and `state.since` Fields, and replaces `PriceRead.price` with `native` and `usd`; nothing was live, so these are renames and removals, not aliases. v3.2 replaces the Now row (§7) and adds `unknown`, `stateReason`, `headMeasuredAt` and `headAgeSec`; the v3.1 Now row survives only as `now.sections` until build-0's page change. v3.1 renames and removes fields (`bondingCurve`, the thesis lane, the situation id) because nothing was live; from admission on, the additive rule below holds. Rule versions travel separately: `projectionVersion`, `materialityVersion`, `lightsVersion`. A threshold change bumps its version, not the contract.
- **Additive** means: new optional properties, new enum members in `type`, `glyph`, `quality` and `source`, new sections in `now`, new lines in `market` beyond the eight. Never: renaming, removing, changing a unit, changing a `Field` to a bare value or back, reordering `lights[]`.
- **A consumer must tolerate:** unknown properties anywhere; unknown event types and glyph families (draw the lane's neutral mark); any `Field` with a null value (print the reason); a `provider:*` field disappearing at the switch date; a `member` block being absent; any state word following any state word; `diffs[]` empty.
- The schemas in this folder are the producer's strict schemas (`additionalProperties: false`), so a typo in M2's output fails in CI. Consumers parse leniently.
- **ETags:** strong, the first 20 hex of sha256 over the canonical JSON (sorted keys) with `meta.etag` blanked. One per document: per situation, the Now list, per member board. Documents are rebuilt on change and debounced 15 to 60 s (T2 §4.5). A `304` is the normal answer to a poll.
- v2 (`/api/room`) is unchanged and keeps its own additive rule.

## 12. Open questions for Bolo, and the choices made

### 12.1 Questions (units, cadence, wave 0 versus wave 1)

1. **`availableAt` for polled sources.** Fomo is polled every 300 s. Is `availableAt` our write time (so up to five minutes after `providerAt`), as the fixtures assume? The order rule (R4) compares `availableAt`, so this sets how often *Money first* is decided by our poll rather than the market.
2. **Retention cadence on Robinhood Chain at wave 0.** R2 needs early-cohort reads inside a 6 to 24 h window. The holdings loop is 120 s budgeted; the pages print "every 30 min". Which cadence can M2 promise at 200 tokens?
3. **Breadth for R3.** Wallets in per window from telemetry, and mentions per token per window (DA-04, M3). Is the mentions rollup wave 0? If not, R3 never fires at wave 0 and the vectors are the only place it exists.
4. **Band p90 tables (M3).** The tape and attention lights need band percentiles. Until M3, propose fixed provisional thresholds (tape: 10 wallets in per minute; attention: 3 mentions in an hour) under `lightsVersion l0-fixed`. Agree?
5. **pump.fun fields.** Units as decimal strings of lamports and 6-decimal base units. Confirm the decoded names (`realSolReserves` or `realSolReserve`), and what "pre-migration token count" means: tokens still sold by the curve (`tokensLeftBeforeMigration` here) or `realTokenReserves` as read?
6. **Pool rows.** Which window does `status` cover (the displayed clock window, or the last hour)? Is `lagSec` the head lag of the collector or of this pool's last row?
7. **Situation id.** `chain:address#openedAt` with `openedAt` = `availableAt` of the opening event, to the second. Fine for M2's key, or do you want the `token#openedAt` form from today's store?
8. **Diffs.** Computed per member on change (fan-out to every watcher) or on read (cached by ETag)? At 30 watched per member, on read looks cheaper.
9. **Safety at wave 0.** Which safety events exist at wave 0: tape-derived `liquidity_pull` and `rug_signature` only? `lp_event` (DA-11), `deployer_move` (DA-10) and contract checks (DA-09) look like wave 1.
10. **Stance flips** need T4 extraction (N3 to N7). Until then the `stanceFlip` materiality clause never fires; fine?
11. **What M2 can compute at wave 0**, my reading: wallets (RH; SOL and BSC partial), traders, callers, attention (Telegram preview, thin), tape (RH, BSC; SOL since 25 Sep), holders (RH only), momentum (RH, BSC), safety (tape-derived only). Correct me per light.
12. **`values` payloads.** Events carry typed `values` (Fields). Is a free map acceptable for M2, or do you want one schema per event type now?

Bolo answered all twelve on 2026-09-27 (`~/.openclaw/wiki/coordination/inbox/vesper/2026-09-27-bolo-contract-v3-review.md`). His answers are binding and are recorded in §12.3.

### 12.2 Choices made where sources disagree (read, then T5, then T8)

1. **AGRIPPA lag: 40 min, not 52.** T5 §1.8: the first Fomo-profiled trader came 40 min after the call. `wireframes-v2` drew the bracket to the minute-60 tape read (52 min). The relationship runs to the first money light, so 40.
2. **AGRIPPA lights on day 2.** `wireframes-v2` shows callers on, traders fading, tape on. With T5's half-lives for $1M to $10M, by 18:10Z on 19 Sep the callers, traders, tape and momentum lights have decayed off; attention (relit 14:30Z, illustrative) and holders stay on. The fixture follows T5's decay.
3. **SPRING coverage on 14 Sep.** The pages print "Telegram preview 5 min, X dark since 18 Sep". On 14 Sep at 17:27Z the Telegram and X sweeps were not running yet (both start 16 Sep, T5 §1.4) and X was not yet dark. Under the nothing-knowable-later rule the fixture shows caller channels as the only attention source and the lag caption says so.
4. **SPRING freeze and cap.** The brief and `design-options/scripts/fixture.py` freeze SPRING at 17:27Z (minute 60) with an illustrative $2.66M cap; `wireframes-v2` uses shifted clocks (minute 107) and $640K. The fixture follows the brief (asOf 17:27:30Z, so the minute-60 read is available). The recurring-wallet count (260) is computed later over later launches, so it is illustrative at 17:27Z; the state sentence does not use it.
5. **Suspect prints do not light safety; four print flags, not one** (rewritten in v3.1 after Bolo item 5). T5 §2.2 lists a suspect print among the safety light's triggers. Here a suspect print is a safety-lane event that feeds `dataQuality` and nothing else. v3 justified that with the repeat rate ("29% repeats on ticks"), which conflated three things. `unchanged` (a price equal to the previous comparable persisted observation), `phantom` (under 0.75x of both neighbours with a covered independent tape check that finds no swap at that price) and `providerStale` (the provider clock did not advance) are different findings, and `misquote` (orientation) is a fourth. Each has its own denominator (§6). A repeat rate says nothing about phantoms, and a candidate without covered tape is `unverified`, never removed. The reason a print does not flip the word is now only that one provider print is not a safety condition; it no longer rests on a repeat rate.
6. **State change is material.** The read's list has five clauses; this contract adds a sixth (the word changed), because the pages lead with it and notifications fire on it (the read §2.8).
7. **Refusal does not force Developing.** The read says a refused row "says Developing". Here only the states that need the refused light are skipped; the others still run (V17). A Solana token with two money lights reads *Money first*. The read's example (MOSSY) still reads Developing because it has one light.
8. **Tie rule (R4e) is new.** Money and attention within one attention cadence cannot be ordered honestly (the read §3.2's quantisation point), so the word is Developing with that sentence.
9. **Decay inflates day-old diffs.** AGRIPPA since 19:30Z the day before has 8 material lines, 4 of them lights that decayed off; the page drew 4. Option: fold decay-offs into one line ("4 lights went off"). Left as separate lines until you and Thomas choose.
10. **Since-you-looked does not pin the state line first.** The brief's sort is material first, then recency, so a state change from the morning sits under the afternoon's retention read. The v2 pages put the state line first. Keep the rule, or pin state lines?
11. **AGRIPPA reopened.** The T5 signal join dates AGRIPPA's first sighting to a watched-wallet event on 17 Sep, 36 h before our tape starts. Under T5 §6 that situation closed and the call on 18 Sep opened a new one; the fixture carries it as `predecessor` with a rule-derived `closedAt`.
12. **Retention reads for AGRIPPA are illustrative.** The only stored RPC truth is from 22 Sep (8 held, 13 reduced, 117 exited of 142). The 19 Sep reads (64, 61, 39, 31) are invented to match the page's story and flagged.
13. **SPRING `asOf` is 17:27:30Z, to the second** (v3.1, §0.12). build-0 read the fixture as 17:27:00Z with minute precision; the fixture was already 17:27:30Z and item 4 above says so. The minute rule is now written down: `asOf` is to the second and nothing is rounded forward.
14. **SPRING's situation id now uses `availableAt`.** v3's generator built SPRING's id from the opening event's `providerAt` (16:28:59Z) while `openedAt` was its `availableAt` (16:29:05Z). v3.1 builds every id from `openedAt` plus the `~k` discriminator, and the validator checks both.

15. **Two sentences, v3.2.** The Now row's `state.sentence` and `whyHere` are the frozen page's wording (pass 7), which the Situation header prints byte for byte. The §3.2 templates stay the Situation state's `sentence`. The two agree on the word, not on the wording; aligning them is a rule change for review, not a contract change. `ruleVersion` on the row names both (`p2-provisional + now wording pass 7`).
16. **The prototype's example rows under the printed materiality rule, v3.2.** The printed rule (About this data) counts a first of something, a 25% price move, 50 entries, a 0.05 top-20 move, a safety change and a state change. The prototype also flagged LUMEN's second and third caller channels and its cohort reductions as material. Under the contract those are observations, so LUMEN reads `1 material since look · 17:02Z` (prototype: 4), its Latest is `state became Early money leaving · 17:03Z` (prototype: third caller channel, 17:21Z), and the order at 17:27Z is MARLIN, AGRIPPA, SPRING, LUMEN, OBOL, KESTREL (prototype: LUMEN second). The grade C rows keep the prototype's own event flags otherwise; price and entry thresholds are applied only to SPRING (grade A), whose strings match pass 7 exactly. Either the example data changes or the rule does; I have not changed the page.
17. **Minute moments, v3.2; settled in v3.4.** v3.2 and v3.3 said the page's rail moment `HH:MM` is `HH:MM:00Z`, while the validator checked the pass 7 strings at `HH:MM:30Z` and build-1 followed the validator. **v3.4:** §7.1 now states the validator's rule: minute `m` of the rail is `asOf - (60 - m)` minutes, seconds inherited from `asOf` (so `HH:MM:30Z` on the fixture); an exact `?t=` clock is labelled `HH:MM:SSZ`. No page string changed (V70).
18. **SPRING's entry is 17:11Z, v3.2.** v3.1's illustrative `entered` event was 17:10Z; the frozen page and the brief use 17:11Z, so the fixture moved it. Both are illustrative.
19. **Price-move anchor, v3.2, closed in v3.3.** v3.3 follows the 23:35 pin (`previous_material`, §7.3, V62); the v3.2 text follows for the record. The brief said "the previous material event of the same kind, else `firstSeenAt`"; the 23:35 pin says "the price at the row's previous material event". On SPRING the two give the same three price moves. The contract follows the brief (`previous_same_kind`); `anchor.kind` is an enum, so switching is one value, if Bolo's tape makes the other cheaper to compute honestly.
20. **Names, v3.2, closed in v3.3.** v3.3 uses the 23:35 names, `estimate` and `history.since`, with the old names removed from the schema. The v3.2 text follows for the record. The 23:35 wording used `estimate` for the market-cap basis and `history.since` for the history start; the brief and this contract use `assumed_supply` and `now.historyFrom`. Same definitions.

21. **The pre-seam fixture, v3.3.** The fixtures keep `usd.basis: tape_usd` with `native.amount` null and a reason, because T5 holds only `priceUsd` and no conversion record. The adapter admits `tape_usd` with an absent native amount only when the absence carries a reason; a present non-stable native asset with `tape_usd` is refused. The brief allowed this; say if the fixture should instead be `unavailable` with its USD removed.
22. **`source` kept on `PriceRead`, v3.3.** The brief's shape lists `{at, availableAt, pool, basis, native, usd}`. `source: tape:<chain>` stays, because it is what refuses a relabelled provider tick in the schema (V53).
23. **Exact product, v3.3.** `native_x_provider` requires `usd.value == native.amount x conversion.quote` as exact decimals (`conversion_mismatch` otherwise), the same rule as the SOL conversion (V58). A producer that rounds must store the rounded value and the quote that gives it.
24. **Empty history before `asOf`, v3.3.** A row with no state history has a state only at `asOf` (the projection's word from evidence at `asOf`); at earlier `t` the state is null with `state history begins at <clock>`, and the row sorts as Developing. The Now document carries no evidence to re-run the projection at an earlier `t`.
25. **Curve reserves and zero, v3.3.** The price boundary refuses zero for prices. For curve reserves the adapter checks the canonical pattern and admits zero, since a reserve of zero can be real and nothing divides by it.

### 12.3 Bolo's answers to §12.1 (binding, 2026-09-27)

1. `availableAt` is the actual persisted or published availability, never copied or backdated from `providerAt`. Fomo streaming intake is not a generic 300 s poll: use the source-specific receipt or capture cadence and transport. Historical clocks that are absent stay null with a reason, never reconstructed from fixture stories.
2. No 200-token retention cadence promise. The 120 s loop cadence is not per-subject balance freshness. Measure actual cohort and head coverage; "30 min" may print only as provisional, with a source-backed timestamp.
3. R3 is blocked until same-window wallet and mention rollups exist with explicit capture completeness. It is not enabled from thin aggregate counters.
4. Fixed light thresholds (`l0-fixed`) are not accepted by this infra pass; thresholds stay under versioned product review. After Q1 an unknown band cannot silently reuse the provider cap.
5. Pre-migration token count = real token reserve still on the curve, raw integer plus verified mint decimals. `complete` is the authoritative completion flag; never manufacture zero reserves. SOL conversion is exact lamports / 10^9 only for a verified native quote. Curve census 0 of 161 resolved pools; no live curve acceptance. (Now in §2.5.)
6. Coverage belongs to the displayed half-open window, per pool. Lag is source-head lag, not trade silence. Historical missing spans carry dates and a reason; a current seek does not heal history. (Now in §2.5.)
7. Preserve the literal chain and address. Second-resolution opening ids can collide for distinct reopenings in one second: add a stable source-event discriminator. (Now §0.14.)
8. Diffs are on read, per member, cached by member + checkpoint + document version; never share a private cache across members.
9. Wave 0 safety covers only types actually emitted with evidence. Swap tape alone does not prove LP withdrawal, deployer moves, contract checks or rug signatures; each is unavailable until its parser or source exists. No "tape-derived safety" promise.
10. Stance flips stay unavailable until extraction, version and evidence are admitted.
11. Wave 0 tonight is not live M2. Wallet, Fomo and caller inputs exist, but event clocks and coverage need retrofits; BSC has historical gaps, Solana capture gaps, no live pump curves, no measured enhanced rows. Tape and momentum are computable only inside covered typed-unit windows. Robinhood Chain holders need actual per-cohort reads. Wave 1 rollups, stance, LP, checks and bands stay separate. Nulls with explicit reasons are the computable result until those facts exist.
12. A typed value map is fine for storage, but producer validation must constrain each known event's economic fields, units and null reasons before computation; the generic `Field` alone is not enough. v3.1 does this for `BondingCurve` (`RawU64`, pinned units); the other event payloads are still generic and are the next producer-side schemas.

### 12.4 Closed with Bolo on 2026-09-27 23:35 (binding, v3.2)

1. **"since seen" is a stable baseline.** `change.basis: since_first_seen` measures from the token's first tape price read ever, `firstSeenAt`, not the first read inside the display window. The baseline survives when that read scrolls out of the 60-minute rail. The rail window and the change baseline are two selectors on the same retained series; the contract carries both `firstSeenAt` and `window.start` (§7.2, V49).
2. **The tilde comes off by validity at `t`, not by a flag.** `marketCap.basis` is `estimate` (v3.3 name; v3.2 wrote `assumed_supply`) unless a supply read exists with `basis: rpc`, decimals, block, `observedAt`, `availableAt`, and a validity interval that covers the moment being rendered. The prototype Boolean `supplyRead` does not appear in the contract. Never back-projected (§7.2, V48).
3. **History has an explicit start.** `now.history.since` (v3.3 name; v3.2 wrote `now.historyFrom`) is the clock at which versioned per-tick rows began being persisted. Now at `t < history.since` refuses with a printed reason, and the rail hatches left of it with the grammar the page already uses for "not on our tape yet". No historical `surfacedAt` is derived from legacy transition rows (§7.1, V47).
4. **Pin: every materiality threshold names its comparison anchor** (§7.3, V54). v3.3: a price move is against the price at the row's previous material event of any kind, as pinned (V62).
5. **Pin: wallets entered are distinct first buys per wallet from tape start**; a second buy is not a second entry; cumulative is the size of that set; bars are first buys whose `availableAt` falls in the interval (§7.4, V50). v3.3: "first" means first observed (earliest `availableAt`), so a past interval never changes (§7.4, V59).

