# Contract v3.8: changes from v3.7

*W-contract-v38, 2026-09-28 20:29 Beirut onward. Brief: `briefs/W-contract-v38.md` (Sprint V21 of `SPRINT-CHAIN-2026-09-27.md`, chained after V20, contract v3.7, and V20b, build-7). Ruling: `~/.openclaw/wiki/coordination/inbox/vesper/2026-09-28-2005-vesper-v21-ruled-sol-usd-reference-read-with-its-own-clock-cap-prints-on-solana-on-a-read-basis.md`, refined 20:13. Trigger: after v3.7 the Solana real document (STONK, `6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx`) carries an admitted supply and a tape quoted in native SOL, and still prints `cap not read` titled `no USD price at this moment`, because every `usd` on its 8,743 reads is `unavailable`. The contract already held the record for a SOL/USD reference read since v3.3 (`PriceRead.usd.conversion`, `$defs.Conversion`); what it did not say is how close a conversion must be to the read it converts. v3.8 adds that one reader rule and the cap's clock and mark. No record type, no key, no rename, no stamp move. This is not a release.*

## How to check this

```
cd /home/botbox/.openclaw/workspace/projects/caverio/companion-2026-09-26
python3 contract/validate.py                     # exit 0: 177 passed, 0 failed (contract/VALIDATE.out; v3.7: 166)
python3 contract/validate.py --v37-compat        # exit 1 by design: 5 of 5 v3.8 vectors fail on v3.7; the pins V94, V95, V96, V99 pass on both
python3 contract/validate.py --v36-compat        # exit 1 by design, byte-identical to v3.7's output (and --v34, --v33, --v32, --v31, --v3)
python3 contract/validate.py --doc build-7/data/real/solana-now.json   # exit 0, document: OK, conversions: 0 not covering
python3 contract/validate.py --doc build-7/data/real/bsc-now.json      # exit 0, document: OK, conversions: 0 not covering
node contract/js/parity-v38.mjs                  # exit 0: 26 of 26 cases, 12 of 12 now_at runs, STONK a and b; writes contract/PARITY-v38.out
node contract/js/parity-v37.mjs                  # exit 0 (V80 to V90 unchanged; it rewrites PARITY-v37.out, see Verification)
(cd contract/legacy-v37 && sha256sum -c SHA256)  # 16 of 16 OK
find build-* design-pass* -newer contract/legacy-v37/SHA256 -type f   # no output
```

"Before" means v3.7 exactly as it stood at 20:30, after build-7 closed (`build-7/CHECKS.md` `Status: DONE`, no build7 runner) and before the first edit. `legacy-v37/` holds the files `legacy-v36/` holds, in the same naming: `nowat_v37.py`, `clocks_v37.py`, `adapter_v37.py`, `schemas_v37.py`, `projection_v37.py`, `coverage_v37.py`, `board_v37.py`, `validate_v37.py`, `vectors_v37.py`, `companion-v37.schema.json`, `now-v37.schema.json`, `COMPANION-CONTRACT-v37.md`, `state-vectors-v37.json`, `now-v37.json` (the v3.7 `fixtures/now.json`) and `VALIDATE-v37.out`, plus `nowat-v37.js` (the v3.7 `js/nowat.js`, byte-equal to `build-7/lib/nowat.js`). `legacy-v37/SHA256` lists all 16, relative to that folder as `legacy-v36/SHA256` is. Key hashes: nowat `e5b793dc…`, clocks `d56ef6b8…` (unchanged since v3.4), JS reader `f37578a0…`, contract `ab0599b2…`, vectors `64918f1d…`, VALIDATE `a4203cfc…`. A v3.8 vector counts only if it passes on v3.8 **and** fails on v3.7, except the four pins, which restate kept rules and must pass on both.

**Vector ids.** V91 to V99, 99 in total. Each carries `briefId` and `since: v3.8`; the pins also carry `pin: true`. V01 to V90 are byte-equal to v3.7 (`v37-vectors-unchanged`).

## Item 1. The coverage rule (§7.2, `nowat.usd_at`, its JS mirror)

| | File and line (v3.8) | Before (v3.7) | After (v3.8) |
|---|---|---|---|
| **The constant** | `nowat.py:109` `CONVERSION_COVER_MINUTES = 60`; `js/nowat.js:197` the same name and value | none | one named constant in each reader, printed in the §7.2 sentence. `validate.py` `check_v38` fails if the two readers or the contract disagree |
| **The test** | `nowat.py:439` `covers(r)`; `js/nowat.js:293` `covers` | none | true unless `usd.basis` is `native_x_provider` and `|conversion.at - read.at|` is more than 60 minutes. Two-sided, the bound included. A clock that does not parse covers nothing |
| **USD at t** | `nowat.py:494` `usd_at`; `js/nowat.js:303` `usdAt` | a `native_x_provider` value counts once `conversion.availableAt <= t`, whatever `conversion.at` is | counts only when all three hold: `conversion.availableAt <= t` (unchanged), `conversion.at <= conversion.availableAt` (`clocks.admit_pair`, unchanged, restated), and `covers`. Otherwise None at every `t` |
| **USD as recorded** | `nowat.py:451` `_usd_static`; `js/nowat.js:301` `usdStatic` | the stored value | None when the conversion does not cover (Choice 3). Used by `price_at`, `price_moves` and `price_text` without `t` |
| **The reason** | `nowat.py:108` `R_USD_COVER`; `js/nowat.js:196`; contract §10 | none | `usd conversion does not cover the read's clock; native quote retained`. `market_cap_at` (`nowat.py:560`) returns it instead of `no USD price at this moment` when the last read at `t` is a non-covering conversion, so `cap_display_at`'s v3.7 title rule prints it (V92). `gaps_at` (`nowat.py:514`) puts it on that read's gap instead of `usd conversion unavailable` |
| **The §7.2 sentence** | Contract §7.2, `clock.priceReads[]` row, **v3.8, coverage** | none | the three conditions, the constant with its value, why 60 minutes (a live reference minutes after the last swap, an hourly range point up to an hour before a read), and that a non-covering conversion is not a schema error |
| **`--doc`** | `validate.py:304` (`_cov`) and the report lines | `supply: N refused` | also `conversions: N not covering`, each with its row, index, read clock and conversion clock. Reported, not refused: the exit code is unchanged (0 on both real documents, which carry no conversion) |

## Item 2. Never back-projected, with the two clocks (§7.2)

| | File (v3.8) | Before (v3.7) | After (v3.8) |
|---|---|---|---|
| **The sentence** | Contract §7.2, `clock.priceReads[]` row, **v3.8, never back-projected** | the availability rule only (`conversion.availableAt <= t`) | `conversion.at` is the provider's clock for the quote, `conversion.availableAt` when we held it. A quote whose `at` is in the past (a range endpoint read today for a read two days ago) is a read of the provider's record: admitted, and counted only at `t >= conversion.availableAt`; before that the row prints exactly what it printed without it. Nothing is applied to a moment before we held it |
| **The code** | `nowat.usd_at`, unchanged in this respect | | the availability gate is the same line as in v3.3. V94 pins it: at `T + 4 min`, after `conversion.at` and before `conversion.availableAt`, `cap not read` titled `no USD price at this moment`; at `T + 5 min`, the figure |
| **The real case** | `PARITY-v38.out`, last section | | STONK with an illustrative conversion held at 20:40Z, after the document's `asOf` 13:27:42Z: at `asOf` the row prints `cap not read`, `no USD price at this moment`, and price `no USD price at this moment`, exactly what it prints today. Held at 13:27:40Z instead: `$42.95M`, clock `2026-09-26T10:00:00Z`, mark `coingecko` (quote 150, illustrative, not a provider figure) |

## Item 3. The cap's clock and mark (`cap_display_at`, `capDisplayAt`, §7.6a)

| | File and line (v3.8) | Before (v3.7) | After (v3.8) |
|---|---|---|---|
| **The return value** | `nowat.py:765` `cap_display_at`; `js/nowat.js:552` `capDisplayAt` | `{text, title, basis}` | `{text, title, basis, clock, mark}`. With the figure: `clock` is the oldest of the priced read's `at`, `conversion.at` when its USD is `native_x_provider`, and the supply read's `observedAt`, returned as written (the first on a tie, in that order); `mark` is the provider in `conversion.source` (`provider:coingecko` gives `coingecko`) when the USD is `native_x_provider`, else null. With `cap not read`: both null |
| **What is unchanged** | | | `text`, `title` and `basis` are v3.7's on every input (`check_v37` compares those three keys to v3.6, `check_v38` to v3.7). `price_text` and `readout_text` are not touched |
| **The §7.6a sentence** | Contract §7.6a, **v3.8, the figure's clock and mark**; §10 display strings | none | every surface that prints the figure prints its clock as `as of <hh:mm>Z` (or the date when the clock is not on the day of `t`) and, when `mark` is set, the provider's mark from `rights/ATTRIBUTION-MARKS.md` (CoinGecko: `Powered by CoinGecko`, linked to `https://www.coingecko.com/`, 12 px rendered, 10 px legal floor), in that order, on the same line as the figure or directly under it. No surface prints a cap without its clock |
| **Schema** | | | none. `clock` and `mark` are reader outputs, not document fields; no schema change was needed (Item 6) |

## Item 4. Reasons (§10)

One entry under **Native quote and USD (v3.3)**: `usd conversion does not cover the read's clock; native quote retained`, with the constant named. One display line under **Printing a cap**: the figure's `as of <hh:mm>Z` and the mark. Nothing else: `reference stale` and `reference absent` from the 20:05 note are not used, and `no USD price at this moment` and `usd conversion not available at this moment; native quote retained` already cover absence.

## Item 5. Vectors: see the table below (V91 to V99)

`vectors.py` `neg38` (`:1018` onward, helpers `_tp`, `_cv`, `_solrd`, `_ssup`, `_covrow`), `state-vectors.json` (99 vectors; its `contract` field stays `companion-v3.7`, as v3.6's stayed `companion-v3.5`), `validate.py`: op `cap_at` (`:808`) now also returns `clock` and `mark` when the reader under test has them (v3.7's does not, so those keys read `absent`); `NEG38` (`:634`), `--v37-compat` (`:156`), `negative-set-v38`, `v37-vectors-unchanged`, and `check_v38` (`:1453`).

## Item 6. Schemas, version, stamp

No key is added or renamed, so the schemas' `$id` and `meta.contractVersion` stay `companion-v3.7`, exactly as v3.6 kept v3.5's stamp: `companion.schema.json` `aeefae9c…` and `now.schema.json` `7ebe24dd61d25f848831cba74877fb30ec0de32608863d9e802ba64033941868` are byte-equal to `legacy-v37/` (checked in `check_v38`), and every v3.7 document is a v3.8 document. Contract §11 says so. The version strings moved to v3.8: the CONTRACT title line and header note, `validate.py` docstring and labels (`v3.8 pass`, `fails on v3.8`), `README.md`, `js/README.md`, the `nowat.py` and `js/nowat.js` headers. No migration script. `make.py` and `vectors.py` rerun leave every schema, fixture and `staging/` file byte-identical.

## Vectors (V91 to V99)

`T` is `2026-09-26T10:00:18Z`, the STONK tape's last read (`max(at)` over `build-7/data/real/solana-now.json`). The read quotes 0.00000006 SOL per token (synthetic), available `T + 2 s`; the conversion is `provider:coingecko`, quote 150.5, so `usd.value` is 0.00000903 exactly; the supply is the STONK read (`814418335339229805`, 9 decimals), observed `T + 30 s` unless stated, valid to `T + 3 h`. The figure, when it prints, is 7,354.20 usd, `$7.4K`.

| Id | briefId | What it pins | v3.8 | v3.7 (legacy-v37/) |
|---|---|---|---|---|
| V91 | I1-cover-after | conversion at `T + 4 min`, available `T + 5 min`; at `T + 10 min`: `rpc_supply`, 7,354.20, `$7.4K`, clock `T`, mark `coingecko` | pass | the figure, no `clock`, no `mark` |
| V92 | I1-cover-61 | conversion at `T + 61 min`, available `T + 62 min`: admitted by `admit_price_read`; at `T + 62 min` and `T + 2 h` `cap not read` titled the new reason, basis `estimate`, clock and mark null; the read's gap carries the new reason; change `no USD price at this moment` | pass | prints `$7.4K` from `T + 62 min` |
| V93 | I1-cover-bound | conversion at `T - 60 min` exactly: counted, clock `T - 60 min`, mark `coingecko`; at `T - 60 min - 1 s`: `cap not read`, the new reason | pass | no `clock`; and prints the figure one second past the bound |
| V94 | I2-availability-pin (pin) | the V91 row at `T + 4 min`: `cap not read` titled `no USD price at this moment`; at `T + 5 min`: `$7.4K` | pass | pass |
| V95 | I2-product-pin (pin) | `usd.value` 0.00000904: `conversion_mismatch`; 0.00000903: admitted (choice 23) | pass | pass |
| V96 | I2-quote-zero-pin (pin) | quote `0`: `price is not a finite positive decimal; not admitted` (rule 18) | pass | pass |
| V97 | I3-oldest-clock | conversion at `T - 30 min` (available `T - 29 min`), supply observed `T - 45 min`: clock `T - 45 min`, mark `coingecko` | pass | no `clock`, no `mark` |
| V98 | I3-tape-usd-no-mark | a USDC-quoted `tape_usd` read at `T`: supply observed `T + 30 s` gives clock `T`; observed `T - 45 min` gives clock `T - 45 min`; mark null in both | pass | no `clock`, no `mark` |
| V99 | I3-no-baseline-pin (pin) | first read `T - 20 min` with no USD, last read `T` converted: `change_at` `no USD price at this moment`; the cap still prints | pass | pass |

`--v37-compat`: exit 1, `5 of 5 negative vectors fail on v3.7; 4 pins pass on both, as they must`. `validate.py` requires a pin to pass on v3.7 as well, and a pin passing does not count toward the compat exit.

## For Bolo (B10)

Nothing here changes B10. His `flow.intervals[]` USD stays the tape's own conversion (§7.7: `priceUsd` from the tape writer, never a provider figure), and no `provider:*` price enters a flow sum. If his tape ever carries a provider conversion per swap, it goes on the read as a `Conversion` (`{source: provider:<name>, quote, at, availableAt}`) with both clocks, and this coverage rule applies to it: within 60 minutes of the swap's clock, counted from `availableAt`. He is told this in H7 terms when he files, not before.

## Choices you may want to overrule

1. **Sixty minutes, two-sided, the bound inclusive.** The brief's number. Two-sided because a live reference fetched minutes after the last swap must cover it and an hourly range point up to an hour before a read is the finest a two-day-old range returns. Inclusive, so an hourly point exactly on the hour covers (V93). One constant in each reader; a change is one line in each and a vector edit.
2. **A non-covering conversion is admitted, not refused.** `admit_price_read` still admits it (V92, case `admitted`); only its USD never counts. The document stays schema-valid and honest, the page prints the reason, and `--doc` reports it without failing.
3. **`_usd_static` applies coverage too.** `price_at`, `price_moves` and `price_text` without `t` read the stored USD with no clock gate. A non-covering USD is "unavailable at every `t`", so it is None there as well, and a non-covering read can never make a price move or an anchor price. No document on disk carries a conversion, so this changes no output.
4. **The cap title names coverage only when the last read at `t` is the non-covering one.** `market_cap_at` prices from the last USD read at `t`; when there is none, the title is the new reason if the last read at `t` is a non-covering conversion, else `no USD price at this moment`. An earlier non-covering read with a later read that simply has no USD keeps the general reason. Its gap still names coverage.
5. **The clock is returned as written, not normalised.** The oldest of the three is chosen by parsed instant and returned as its source string (so a fraction or an offset survives), which keeps Python and JS byte-equal without a formatter. Surfaces format it as `as of <hh:mm>Z`.
6. **V99 is a pin.** The brief lists it without the word, but a converted last read with an unconverted first read gives `no USD price at this moment` for change on v3.7 as well (the v3.3 base-read rule), so it cannot fail on v3.7. It restates that a later conversion invents no baseline.
7. **`--v37-compat` was added** like `--v36-compat`, so the pass-here-fail-there rule is checked the same way for V91 to V99. `NEG31` now excludes `since: v3.8`, so `--v3-compat` still runs only V37 to V41 (its output is byte-identical to v3.7's).

## Verification

- `python3 contract/validate.py`: exit 0, **177 passed, 0 failed** (v3.7: 166). New: `negative:V91` to `V99`, `v37-vectors-unchanged`, and `cover-v38`: the constant and reason the same in `nowat.py`, `js/nowat.js` and the contract; both schemas byte-equal to `legacy-v37/`; 11 price reads in V91 to V99 schema-valid, V92's non-covering one included; 742 row-moments over `fixtures/now.json` and build-3's `now-supply.json` (rail minutes 0 to 60) and build-7's two real documents (minutes 0, 15, 30, 45, 60) equal to v3.7 in `market_cap_at`, `cap_display_at`'s three keys, `gaps_at` and `change_at`; 0 `native_x_provider` reads on disk; 42 printed figures each with a clock at or before `t` and no mark; every `cap not read` with clock and mark null. `check_v37` now compares `cap_display_at`'s three v3.7 keys to v3.6 (`_v37_keys`) and passes as before. `contract/VALIDATE.out` is this run.
- `--v36-compat`, `--v34-compat`, `--v33-compat`, `--v32-compat`, `--v31-compat`, `--v3-compat`: exit 1 each, each output byte-identical to v3.7's (captured before the first edit, compared with `cmp`). `--v37-compat`: exit 1, 5 of 5, 4 pins.
- `validate.py --doc build-7/data/real/solana-now.json`: exit 0, `document: OK`, `supply: 0 refused`, `conversions: 0 not covering`, STONK `cap not read (no USD price at this moment)`. The same for `bsc-now.json` (螃蟹). Both are v3.8 documents unchanged.
- `migrate_v36_to_v37.py --check`, `migrate_v34_to_v35.py --check`, `migrate_v32_to_v33.py --check`: exit 0.
- **Parity:** `node contract/js/parity-v38.mjs` exit 0, `contract/PARITY-v38.out`: **26 of 26 cases byte-equal** (V80, V81 twice, V82, V89 twice, V91 to V99 in 17 cases, KESTREL three times; `cap_display_at`'s keys all compared, `clock` and `mark` included), **12 of 12 `now_at` runs** (build-3's nine, the contract fixture, build-7's two real documents, canonical JSON with build-7's `canon.mjs`), and the STONK row with an illustrative conversion held before and after `asOf`: equal. `node contract/js/parity-v37.mjs` still exits 0 with 9 of 9, 12 of 12 and the BSC row equal; rerunning it rewrites `PARITY-v37.out` with the two new keys in each `display`, so the file was put back to v3.7's committed bytes (`git checkout`) after the run.
- **build-3 parity guard** (`/tmp/v38/parity-guard.py`): `build-3/scripts/dump-nowat.py` on the v3.8 `nowat.py` is **9 of 9 byte-equal to the same script on legacy-v37's `nowat_v37.py`**, and reproduces build-3's `parity-*.py.json` byte-equal on 5 of 9 raw and 9 of 9 once `wallets.reason` (null on every row, added by v3.6) is taken out: exactly v3.7's result, for v3.6's reason. `now_at` does not call `cap_display_at`, so v3.8 changes none of its bytes.
- **Generators:** `make.py` and `vectors.py` rerun: every schema, fixture and `staging/` file byte-identical (`sha256sum -c` against a snapshot taken before the rerun); `state-vectors.json` V01 to V90 byte-equal to `legacy-v37/state-vectors-v37.json`. Hashes: `nowat.py` `80f64245…`, `js/nowat.js` `998665dd…`, `state-vectors.json` `5e4578fe…`, `COMPANION-CONTRACT-v3.md` `f6951965…`, `fixtures/now.json` `7610134b…` (unchanged).
- `(cd contract/legacy-v37 && sha256sum -c SHA256)`: 16 of 16 OK.
- No file under any `build-*` or `design-pass*` directory changed: `find build-* design-pass* -newer contract/legacy-v37/SHA256 -type f` returns nothing. build-7's verify pins `contract/` byte for byte, so it now differs by design; build-8 re-pins. No deploy. Nothing sent. No network call. No em dashes in any file written here (`grep -rn $'\xe2\x80\x94'` over them: nothing).

## Still open (build-8, not in this brief)

- **One SOL/USD reference read from CoinGecko** `coins/solana/market_chart/range` for the hour around the STONK read at `2026-09-26T10:00:18Z` (the range for two days back returns hourly points). Take the point nearest the read; it must be within 60 minutes of `10:00:18Z` or it will not cover.
- **Store it as a `Conversion` on that read** (`clock.priceReads[]` entry with `at` `2026-09-26T10:00:18Z`, index 8540 in build-7's file): `source: provider:coingecko`, `quote` the provider's value as a decimal string, `at` the provider point's clock, `availableAt` the fetch clock; `usd.basis: native_x_provider`, `usd.value` the exact product of `native.amount` (`0.0003515628686154278`) and the quote, `usd.reason` null. Convert only that read: the priced read is the last one in `availableAt` order with USD at `t` (§7.1 `last(t)`), not the one with the largest `at`.
- **The document's `asOf` must be at or after the fetch clock, with a supply read valid there.** A conversion counts only from its `availableAt` and nothing is read after `asOf`, so a fetch made after build-7's `asOf` (13:27:42Z) cannot print at that `asOf` (`PARITY-v38.out`, STONK b). build-7's supply read is valid only from 13:27:37.514Z to 13:27:45.474Z, so build-8 needs a fresh `getTokenSupply` read valid at its new `asOf`, or the cap stays `cap not read` for the supply.
- **The cap then prints** with `clock` the older of the provider point's clock and `2026-09-26T10:00:18Z` (26 Sep, about 10:00Z; printed as the date, since it is not on the day of `t`), and `mark` `coingecko`, so the page prints `Powered by CoinGecko` linked, at the caption floor, after the clock (§7.6a). Route the cap slot on Board and Market through the v3.8 `capDisplayAt` (copy `contract/js/nowat.js` into `lib/`) and print the two new keys.
- **Provenance pinned in the document's SOURCES:** the request URL and parameters, the response bytes' sha256, the fetch clock, the chosen point and its distance from the read, and the product.
- **BSC unchanged:** its quote is oANTHROPIC, no provider converts it, and it stays `cap not read`, `no USD price at this moment`.
- Re-pin build-8's verify to v3.8's `contract/` bytes. The schema pin `7ebe24dd…` is unchanged.
- Everything in CHANGES-v3.7 "Still open" that build-7 did not close stands.

Status: DONE
