# Price Predict: full handover for external review

Prepared for a cold reader (ChatGPT) who has never seen this system.
All times are stated in UTC with Bangkok time (UTC+7) in brackets.
All blockchain addresses are written in full, never truncated.
Prepared 19 Sep 2026 06:10 UTC (13:10 Bangkok).

---

## 0. What I want reviewed

Read section 11 first if you are short on time. The short version:

1. The headline result says no exit rule clears the cost hurdle. I want that attacked.
2. A live data defect found while preparing this document contaminates the Solana ledger. I want a second opinion on whether the guard I applied is the right guard.
3. The analysis script that produced the original headline numbers no longer exists. The numbers in section 5 cannot currently be reproduced from source. Section 5b is a fresh, independent recomputation from exported rows, done today.
4. There is an open decision (section 10) about which single rule to freeze for a forward test.

Raw data links are in section 12. They are live HTTP URLs and they work without a login.

---

## 1. What Price Predict is

Price Predict is a paper trading laboratory for brand new crypto tokens.

It watches two chains continuously, records every new token it can see, writes down a simulated $100 position at the moment a rule says "this one looks interesting", then comes back later and measures what actually happened to the price. Nothing is ever bought. There is no live capital in this system.

The one question it exists to answer:

> Is there any entry rule plus exit rule combination that returns more than $110 for every $100 staked, after slippage and fees, measured over a population that was fixed before the measurement?

$110 is the hurdle, not $100. The extra $10 is the margin demanded for the effort and the risk. A rule returning $104 is a rule that does not exist.

### The population rule

This is the most important discipline in the project and it was learned by getting it wrong repeatedly.

Every score must be `peak / price_at_decision`, computed over a population that was defined before anyone looked at the outcomes. Never an absolute market cap. Never a set assembled after the fact.

Every significant error in the history of this project reduces to the same shape: a number computed over whatever set happened to be in front of me, then presented as if it described the world.

### Silent zeros

The second recurring failure. A field that is missing, renamed or mistyped returns 0.0, and a 0.0 that means "no data" is indistinguishable from a 0.0 that means "measured zero". Several defects in section 6 are this exact failure.

---

## 2. Where it runs

Single DigitalOcean droplet, internal hostname `kookwatch`, IP address 159.223.88.168.

Resized on 18 Sep 2026 from 1 vCPU / 961 MB RAM / 24 GB disk to 2 vCPU / 3,915 MB RAM / 77 GB disk ($24 per month). The old box was thrashing and starving the collectors; that was the root cause of a week of degraded collection.

State as of 19 Sep 2026 06:04 UTC (13:04 Bangkok):

```
uptime        1 day, 21 minutes
load average  5.56, 5.97, 6.24
memory        3,915 MB total, 1,971 MB used, 2,060 MB cache
disk          77 GB total, 25 GB used, 52 GB free (33%)
services      trench.service      active, enabled
              trench-sol.service  active, enabled
```

A second droplet named `bk-bots` (159.223.77.212) was found to be an abandoned box. It was snapshotted and destroyed on 18 Sep 2026.

### Two chains, one codebase

| | Robinhood Chain | Solana |
|---|---|---|
| service | `trench.service` | `trench-sol.service` |
| database | `/opt/trench/trench.db` (8.69 GB) | `/opt/trench-sol/trench.db` (5.35 GB) |
| tape cache | `/opt/trench/kl.db` (29.9 MB) | `/opt/trench-sol/kl.db` (73.5 MB) |
| env | `TRENCH_CHAIN=robinhood` | `TRENCH_CHAIN=sol` |

Both run the same Python files out of `/opt/trench`. `cfg.py` reads `TRENCH_CHAIN` and `TRENCH_DB` and picks the database. The two populations are never pooled. A figure that mixes them is a bug.

### The rate limiter

Everything that talks to the market data provider (GMGN) goes through one shared leaky bucket, `/opt/common/gmgnlimit.py`, with its state in `/opt/common/gmgn_state.json` guarded by a file lock.

```
RATE  = 6.0   tokens per second
CAP   = 20.0
FLOOR = 4.0   low priority callers may not spend below this
```

Call weights: rank 1, token_info 1, kline 2, trenches 3.

A HIGHER `reserve` value means LOWER priority. Collectors run at reserve 0. Settlement runs at reserve 6. Backfill runs at reserve 10.

Raising RATE is not an option. 14 calls per second earned a box wide IP ban on 15 Sep 2026.

### The 25 hour cliff

`gmgn.kline(addr, chain, resolution, limit)` accepts a `limit` and nothing else. There is no start time parameter. 1,500 one minute candles is therefore a 25 hour window that always ends NOW.

Consequence: if a token's tape is not fetched and cached within 25 hours of the entry, that token's outcome can never be computed. Ever. This is why `kl.db` exists and why settlement backlog is not merely "late data", it is permanent data loss. This single fact drives most of the engineering in section 7.

---

## 3. Data model

Two SQLite databases per chain. Row counts below are from 19 Sep 2026 06:03 UTC (13:03 Bangkok).

### The tables that matter

**`paper`** is the main ledger. One row per simulated position taken by the primary entry route.

```
addr TEXT, ts INTEGER, age_s INTEGER, born INTEGER, pad TEXT, density INTEGER,
mcap REAL, liq REAL, holders REAL, eye REAL, smart REAL, prog REAL,
score INTEGER, sigs TEXT, has_vec INTEGER, book_a INTEGER, book_l INTEGER,
book_b INTEGER, slip REAL, n_after INTEGER, last_mcap REAL, last_ts INTEGER,
peak REAL, peak_ts INTEGER, dead INTEGER, ret TEXT, ripe TEXT,
scored_ts INTEGER, agree INTEGER, size REAL, tradeable INTEGER, gross TEXT,
settled INTEGER, settle_ts INTEGER, peak_true REAL, n_candles INTEGER,
peak_obs REAL, stars REAL, stars_age INTEGER, dep TEXT, dep_n INTEGER,
dep_win INTEGER
```

Key columns explained, because several are easy to misread:

- `ts` is the entry time, unix seconds. `age_s` is how old the token was at entry.
- `mcap` is the market cap at the moment of decision. This is the denominator in every multiple.
- `slip` is the modelled slippage for the position size. `size` is the stake, always 100.0.
- `tradeable` is 1 when the position passed the fill model. Rows with `tradeable=0` are never settled.
- `peak` is the naive peak. `peak_true` is the peak derived from the per minute kline tape. `peak_obs` is the peak read directly off the observation board. **`peak_obs` is the only independent witness**, because it reads market cap off the board rather than deriving it from a price ratio.
- `n_candles` is how many kline candles were available. 0 means the tape was gone.
- `ret` and `gross` are JSON blobs holding the return for each exit rule, as a multiple of stake. `0.2874` means 28.74 cents returned per dollar staked. `ret` is net of slippage and fees; `gross` is before them.
- `settled` is NULL, 1 or 2. NULL means never settled. 1 means first settle done (the 70 minute stage). 2 means final settle done (the 25 hour stage).

Example `ret` blob:

```json
{"hold_1h": 0.2874, "hold_6h": 0.2703, "hold_24h": 0.2747, "tp2x": 0.2747,
 "tp3x": 0.2747, "ladder": 0.2747, "trail50": 0.3584, "peak": 0.9579}
```

**`paperx`** is the same shape for a second entry route, the one driven by social calls. It adds:

```
called INTEGER, caller TEXT, caller_followers INTEGER, caller_breadth INTEGER,
call_age_s INTEGER, entry_vs_m1 REAL, book_c INTEGER, book_d INTEGER
```

**`obs`** is the raw observation tape, one row per token per poll. This is the independent witness.

```
ts INTEGER, addr TEXT, col TEXT, mcap REAL, liq REAL, prog REAL,
holders INTEGER, j TEXT, eye REAL, smart REAL, call REAL, netbuy REAL,
ratrate REAL
```

**`tok`** is the token registry: address, symbol, name, creation time, launchpad, creator wallet, funder wallet.

**`kl`** in the separate `kl.db` is the cached kline tape, one row per address per entry, holding the candle array as JSON in column `j`.

### Row counts, 19 Sep 2026 06:03 UTC (13:03 Bangkok)

| table | Robinhood | Solana |
|---|---:|---:|
| `paper` | 58,574 | 89,625 |
| `paperx` | 39,801 | 16,870 |
| `obs` | 9,350,942 | 3,580,452 |
| `tok` | 97,023 | 176,013 |
| `kl` (separate db) | 15,141 | (not counted this pass) |

Other tables present in the Robinhood database, with row counts, for completeness: `call` 126,511, `evt` 10,911, `kindrate` 128, `kindtag` 13,383, `lab` 15,851, `model` 382, `narterm` 7,328, `nbobs` 3,801,383, `nbtok` 11,389, `outcome` 19,780, `pred` 61,520, `recon` 23,806, `sboard` 119, `snap` 60,142, `wal` 436,295, `wallead` 137,132, `walpair` 183,074, `walpull` 29,384, `walrep` 56,622, `xasked` 1,264, `xauthor` 247, `xpost` 603.

### Coverage window

| | first row | last row |
|---|---|---|
| Robinhood `paper` | 13 Sep 2026 19:03 UTC (14 Sep 02:03 Bangkok) | 19 Sep 2026 05:57 UTC (12:57 Bangkok) |
| Solana `paper` | 14 Sep 2026 05:54 UTC (12:54 Bangkok) | 19 Sep 2026 06:02 UTC (13:02 Bangkok) |

So the usable history is roughly five days per chain. This is young. Any conclusion drawn from it carries that caveat.

---

## 4. The pipeline

### 4.1 Collection

`trench.py` runs as a long lived service per chain. It polls the provider for new and trending tokens, writes one `obs` row per token per poll, and registers unseen tokens in `tok`. Normal poll rate is roughly 143 cycles per hour on Robinhood and 89 on Solana.

An hourly prune trims the JSON payload column on `obs` rows older than `OBS_PAYLOAD_DAYS` (currently 4). The prune is chunked at 5,000 rows, checkpoints the write ahead log between chunks, sleeps 2 seconds between chunks and stops after `PRUNE_CHUNKS` (currently 40) chunks per pass.

### 4.2 Entry

`paper.py` runs every 5 minutes from cron (`3-59/5 * * * * /opt/trench/paper.sh`). It evaluates the entry rules against currently visible tokens and writes a `paper` row for each token that qualifies. `late.py` does the same for the later age route.

Because the full stream is far larger than settlement capacity, arrivals are sampled:

```python
KEEP_RATE = float(__import__("os").environ.get(
    "TRENCH_KEEP", "0.07" if cfg.CHAIN == "sol" else "0.15"))

def keep_coin(addr):
    if KEEP_RATE >= 1.0:
        return True
    h = hashlib.sha1(str(addr).encode()).hexdigest()
    return (int(h[:8], 16) / float(0xFFFFFFFF)) < KEEP_RATE
```

This is a **stable hash threshold, not a random draw**. That matters more than it looks: because the sample is a threshold on a deterministic hash of the address, a smaller `KEEP_RATE` produces a strict SUBSET of a larger one. Data collected at 0.5 can therefore be combined with data collected at 0.07 by filtering the old data to the same threshold. It can never be combined by raw concatenation, because that would over represent the earlier period.

`paper.py` and `late.py` both call `keep_coin`, so both legs admit exactly the same coins.

### 4.3 Settlement

`settle.sh` runs every 5 minutes (`1-59/5 * * * * /opt/trench/settle.sh`). It runs the two chains in parallel and the two tables within a chain in series, with a total budget of 270 seconds and a floor of 40.

`settle.py` picks the batch. The selection query is the heart of the system, so it is reproduced in full:

```sql
SELECT addr, ts, mcap, slip, COALESCE(settled,0) FROM <table>
WHERE tradeable=1 AND ret IS NOT NULL AND (
     (COALESCE(settled,0) < 1 AND (? - ts) >= ?) OR
     (COALESCE(settled,0) < 2 AND (? - ts) >= ?) OR
     (COALESCE(settled,0) = 1 AND (? - ts) < ?
        AND (? - COALESCE(settle_ts,0)) > ?))
ORDER BY
  CASE WHEN (? - ts) >= ? THEN 0
       WHEN COALESCE(settled,0) = 0 THEN 1
       ELSE 2 END ASC,
  CASE WHEN (? - ts) >= ? THEN ts ELSE -ts END ASC
LIMIT ?
```

Stages: STAGE1 is 70 minutes, STAGE2 is 25 hours, REFRESH is hourly.

Priority classes:

- **0**: the final 25 hour horizon is due. Sorted OLDEST FIRST, because the tape is expiring.
- **1**: never settled at all. Sorted newest first.
- **2**: the hourly refresh. Newest first.

Two things follow from that `WHERE` clause and they are easy to get wrong when auditing:

- Rows with `tradeable=0` or `ret IS NULL` are **never** settled and must not be counted as backlog.
- `settled` is NULL for unsettled rows, so `WHERE settled = 0` matches nothing. Any audit written as `settled=0` silently reports a backlog of zero. **I made exactly this mistake yesterday and reported a wrong figure to the operator. It is corrected in section 8.**

Settlement fetches the kline tape (cached into `kl.db`), computes `peak_true` and the per exit returns, and writes `ret`, `gross`, `peak_true`, `n_candles`, `peak_obs`, `settled` and `settle_ts`.

The current GMGN priority for settlement:

```python
gmgn.RESERVE = float(__import__("os").environ.get("SETTLE_RESERVE", "6.0"))
```

---

## 5. The headline result (Gate A)

Gate A was the retrospective measurement: take everything collected so far, apply an honest fill model (closes only, no touching intrabar highs, slippage and fees charged), and see whether any exit rule clears $110 per $100.

Final run, Robinhood 5,339 positions, Solana 14,164 positions:

| exit rule | Robinhood | Solana |
|---|---:|---:|
| A: hold 1 hour | $61.22 | $84.81 |
| B: sell at 60 seconds | **$87.71** | **$100.27** |
| C: 2x or minus 50%, else out at 10 minutes | $81.72 | $91.00 |
| D: half at 2x, rest at $500k cap | about $76 | $92.33 |

Fraction that ever reached a $1,000,000 market cap:

- Robinhood 0.09% (5 of 5,338)
- Solana 0.64% (88 of 13,827)

**Nothing clears the $110 hurdle.** The best cell in the table is Solana selling at 60 seconds, at $100.27, which is a coin flip before anyone's time is priced.

Robustness check that was run: `FIT_ONLY=1` restricts the ledger to rows with an independent witness. Every figure moves UP under that restriction, so the negative result does not rest on unverifiable rows. That is the correct direction for a robustness check to move, and it is why I believe the negative result rather than suspecting the measurement.

### Environment knobs used by the analysis

| knob | value | meaning |
|---|---|---|
| `HI_CAP` | 3.0 | ceiling applied to a suspicious multiple |
| `VOL_FRAC` | 0.05 | fraction of volume assumed fillable |
| `MAXGAP` | 600 s | largest acceptable gap in the tape |
| `FIT_SPREAD` | 4x | reject a tape derived peak more than 4x the witnessed peak |
| `FIT_ONLY` | flag | restrict to witnessed rows only |
| `FIT_MIN` | **undocumented** | appears in the pipeline; its value and meaning are NOT verified. Treat any result that depends on it as unconfirmed. |

### 5a. Reproducibility problem, stated plainly

The script that produced the table above lived at `/tmp/pp.py` on the droplet. **It was never version controlled and `/tmp` was wiped by the reboot during the resize. It is gone.**

So the Gate A table cannot currently be regenerated from source. It is a recorded result, not a reproducible one. This is a real weakness in the handover and I am not going to dress it up.

What survives: the underlying rows, which are exported and linked in section 12, and the per minute tapes in `kl.db`. A reviewer can rebuild an equivalent ledger from those. The custom exits (A, B, C, D above) were computed from the per minute tape, not from the stored `ret` blob, so reproducing them exactly requires the tape as well as the rows.

### 5b. Fresh independent recomputation, 19 Sep 2026

Because of 5a, I recomputed a ledger today from the exported rows alone, using the stored `ret` blob rather than the custom exits. This is a DIFFERENT and simpler ledger. It is not the Gate A table. It is a sanity check on whether the data supports the same conclusion.

Mean dollars returned per $100 staked, all settled rows, no guard applied:

| exit | RH `paper` n=12,136 | RH `paperx` n=6,225 | SOL `paper` n=48,373 | SOL `paperx` n=7,543 |
|---|---:|---:|---:|---:|
| hold_1h | $72.99 | $72.96 | **$16,641.29** | **$1,167.52** |
| hold_6h | $68.60 | $67.31 | **$6,987.32** | **$41,423.41** |
| hold_24h | $67.28 | $65.11 | **$1,651.66** | **$8,842.03** |
| tp2x | $78.76 | $78.48 | $88.21 | $82.20 |
| tp3x | $77.69 | $76.30 | $87.55 | $80.73 |
| ladder | $76.13 | $75.62 | $89.55 | $87.13 |
| trail50 | $74.43 | $74.65 | **$1,628.08** | **$8,771.07** |
| peak | $139.11 | $153.07 | **$34,703.23** | **$130,779.95** |

The bolded Solana cells are impossible. They are the live defect described in section 6.6. A handful of rows carry a `peak_true` in the hundreds of billions or trillions of dollars and they destroy every mean computed over that column.

Applying the `FIT_SPREAD` style guard (drop any row where `peak_true > 4 x peak_obs`, using the independent witness):

| exit | RH `paper` | RH `paperx` | SOL `paper` | SOL `paperx` |
|---|---:|---:|---:|---:|
| hold_1h | $72.92 | $72.83 | $88.92 | $68.49 |
| hold_6h | $67.70 | $65.44 | $84.95 | $54.82 |
| hold_24h | $66.53 | $63.59 | $83.91 | $50.58 |
| tp2x | $78.44 | $77.86 | $87.07 | $77.51 |
| tp3x | $76.92 | $74.90 | $85.09 | $71.14 |
| ladder | $74.84 | $73.29 | $84.72 | $69.22 |
| trail50 | $73.92 | $73.65 | **$90.81** | $71.54 |
| peak | $132.65 | $139.07 | $150.45 | $202.40 |

Guarded and restricted to rows that have a witness at all:

| | n | hold_1h | tp2x | trail50 |
|---|---:|---:|---:|---:|
| RH `paper` | 10,658 | $71.53 | $77.69 | $72.64 |
| RH `paperx` | 5,292 | $69.74 | $75.58 | $70.70 |
| SOL `paper` | 28,383 | $88.36 | $85.84 | $91.15 |
| SOL `paperx` | 5,978 | $66.72 | $76.78 | $70.09 |

**Conclusion of the recomputation: the Gate A verdict holds.** Best guarded cell anywhere is Solana `paper` trail50 at $90.81 per $100. The `peak` row is the theoretical maximum if you sold at the exact top with perfect foresight, and even that is only $132 to $150 on the main tables. There is no exit rule in this data that returns $110.

Multiple distribution, from the same exported rows, using `peak_true` where present and `peak` otherwise:

| reached | RH `paper` | RH `paperx` | SOL `paper` | SOL `paperx` |
|---|---:|---:|---:|---:|
| 2x | 9.57% (1,161) | 11.49% (715) | 10.16% (4,913) | 23.27% (1,755) |
| 5x | 2.50% (303) | 2.89% (180) | 3.17% (1,535) | 8.09% (610) |
| 10x | 0.87% (106) | 1.16% (72) | 1.65% (800) | 4.52% (341) |
| 50x | 0.06% (7) | 0.10% (6) | 0.58% (279) | 2.20% (166) |

Note that these multiple counts are NOT guarded, so the Solana columns are inflated by the same defect.

Entry market cap, median and 90th percentile:

| | median | p90 |
|---|---:|---:|
| RH `paper` | $5,208 | $23,713 |
| RH `paperx` | $5,462 | $39,113 |
| SOL `paper` | $2,925 | $46,002 |
| SOL `paperx` | $26,716 | $831,298 |

---

## 6. The defect ledger

Every defect below was found by auditing. Note the pattern that matters most:

> **Every single defect found so far resolved missing or ambiguous data in our own favour.**

That is not a coincidence and it is the strongest argument for treating any positive result from this system with suspicion.

### 6.1 to 6.5: defects found in earlier sessions

These were found by auditing individual positions against the raw tape. The token addresses below are the worked examples; each one is a case where the recorded outcome disagreed with what actually happened on chain.

```
Eju51JV3nUmWE2bDPwBzzwYXAugE1JADneV79EZ3keU7
3zmVtrnPBeQDAtAQpReN9Cox4xizzpho2ipdoHKRZgGc
EaDE3kxa8o3c6fvTWdF6XdmthJ5Y2j9zykoEbicLjA3E
2qP1Avi76Kvktu19xFJ4B1GAoDYx8jCgJEf8Pz3HS2vh
FcjHXfhiAgdMkbb6iZnmmxH3G8pSuVxrRHS9vdB9wCGL
6NTPz7iSpz1b363uT8TtRvH84aYxR68EEkGHgJN2Urew
7EqFUJp8h6QsLfZg5d1KwSW17PoUxxqnZDHS2XaafhQ9
AFZhFM8GRAfDGXsKJAqk55jq7EFjQtqgxvmyMrb9pump
```

The defect families were: a peak read from the wrong field returning a silent zero; a peak taken from a window that started after the actual high; an entry price taken from a later candle than the decision; a fill model that assumed depth that was not present; and a horizon that was marked ripe before it had closed. Each was fixed and each fix moved results DOWN.

### 6.6 LIVE DEFECT, found 19 Sep 2026 while preparing this document

**This one is not fixed. It is writing bad data right now.**

`peak_true` on Solana is producing physically impossible values. Worked example, the worst row in the export:

```
addr        E8ULWeXuVboG9p1jAErAGoFWxu7acCJ5jNSMidxYfba9
entry ts    1789648065  (18 Sep 2026 12:27 UTC, 19:27 Bangkok)
entry mcap  $24,062,678
liquidity   $10,010.21
peak_true   $205,811,000,000,000        (205.8 trillion dollars)
peak_obs    $33,171,740                 (the independent witness)
n_candles   22
last_mcap   $88,160,960
ret.hold_1h 7179.245                    ($717,924 returned on a $100 stake)
```

The independent witness says this token went from $24.06M to $33.17M, a 1.38x. The tape derived figure says it went to $205.8 trillion, a multiple of 8.5 million. The witness is obviously right.

Six more of the top eight offenders, all Solana, all with the same signature (entry market cap around $57,000 to $59,000, liquidity around $22,500, a tape of only 3 to 9 candles, and a `peak_true` in the hundreds of millions or billions):

```
E1WakJTcUmzsaPTMjkGFu9DZsXLX2NvPQLGrxX4DU8ms   peak_true $1,637,587,000   peak_obs $63,529.94    n_candles 5
FVGcEyyvnWwrep2Z58xUqnawCjYso4Ab2TrZR4V4cBq9   peak_true $468,372,600     peak_obs $907,938.87   n_candles 9
2yoxrMqKTxR16yvErrjfBvTuBEqMrrdqZPUV2Tp29mK1   peak_true $3,076,449,000   peak_obs $67,088.42    n_candles 4
J6mBMauRLFMTMkRcSZAkYW7oYCWv6YYTwPTMPKW6pxN4   peak_true $373,966,700     peak_obs $69,811.63    n_candles 3
EYEmoiNHb9bL2TD9jeSrtLhN1kjygCYZXeTS6k9MgQpw   peak_true $331,165,700     peak_obs $74,554.71    n_candles 5
Eju51JV3nUmWE2bDPwBzzwYXAugE1JADneV79EZ3keU7   peak_true $319,882,400     peak_obs $121,947.61   n_candles 5
CE1nTkKyrQyDs6Bb3mjLhKw2NLGWN2qQKYNyoUwrDBe2   peak_true $293,191,400     peak_obs $60,061.30    n_candles 9
```

Note that `Eju51JV3nUmWE2bDPwBzzwYXAugE1JADneV79EZ3keU7` appears in the earlier defect list at 6.1. It was flagged before and it is still producing a corrupt `peak_true`. Whatever was fixed for it did not cover this path.

Scale of the contamination, measured 19 Sep 2026:

| table | rows with `peak_true > 4 x peak_obs` | rows with NO witness at all |
|---|---:|---:|
| RH `paper` (12,136) | 185 (1.52%) | 1,293 |
| RH `paperx` (6,225) | 93 (1.49%) | 840 |
| SOL `paper` (48,373) | 1,893 (3.91%) | 18,097 (37.4%) |
| SOL `paperx` (7,543) | 635 (8.42%) | 930 |

On Solana `paper`, only 69 rows have `hold_1h` above 50x, 150 above 10x and 465 above 3x, out of 43,949 with a value. The median is 0.9098 and the mean excluding rows above 50 is 0.8316. So fewer than 0.2% of rows are moving the mean from about $83 to about $16,641.

**Hypothesis, not verified:** the signature (thin tape, Solana only, entry cap clustered tightly) points at a price denomination or decimals mismatch, where the kline is priced in one unit and the entry market cap in another, so the ratio is multiplied by a large constant. The clustering of bad rows around an entry cap of $57,000 to $59,000 with liquidity of $22,500 looks like one specific pool type or migration state. **This needs to be diagnosed before any Solana result is trusted.**

**The 37.4% of Solana rows with no witness at all is arguably the bigger problem**, because for those rows there is nothing to check `peak_true` against. The guard cannot fire. They could be silently wrong in either direction and nothing in the system would notice.

---

## 7. What happened in the last 48 hours

The system degraded badly and was repaired. The sequence matters because several of the repairs were wrong before they were right.

### The root cause

The droplet had 961 MB of RAM and was swapping. RSS understated real memory use by roughly 6x. An early report of "49 Python programs using 828 MB" was wrong; measuring RSS plus VmSwap showed `casig.py` alone at 1,113 MB and the two collectors at 561 MB and 267 MB. That correction was made to the operator explicitly.

The collectors were also the lowest priority processes on the box: `trench.service` at nice 5, `trench-sol.service` at nice 10, and every batch job at nice 0.

### Patch ledger, this session

| patch | file | change | status |
|---|---|---|---|
| 210 | `paper.py` | AST sweep for unguarded database writes | live |
| 211c | `settle.py` | `_need` rule: final horizon uses `ts + STAGE2`, first settle uses `ts + STAGE1`, refresh uses `now` and is never cached | live |
| 212 | `health.py` | settle queue alarm judges the FLOOR (min of last 3 ticks against min of first 3) rather than the last value | live |
| 213 | systemd | batch jobs to nice 15, collectors to nice -5 | live |
| 216 | `trench.py` | `OBS_PAYLOAD_DAYS` env knob, default 4 | live |
| 217 | `health.py` | disk alarm: warn under 8 GB, FULL under 4 GB, print GB per day and days to full | live |
| 218 | `trench.py` | hourly prune chunked at 5,000 rows, checkpointed between chunks, capped at 40 chunks per pass, 2 second sleep | live |
| 219 | `trench.py`, cron | emergency limits unwound after the resize; 12 hourly restart of both collectors | live |
| 220 | `settle.sh` | `TOTAL=270` (was 240), `FLOOR=40` | live |
| 221 | `settle.py` | concurrent tape prefetch, 4 worker threads | **built, deployed, rolled back within 10 minutes** |
| 222 / 222b / 222c | `settle.py` | settlement reserve lowered to give settlement more budget | **reverted, caused a collector blackout** |
| 222d | `settle.py` | reserve restored to 6.0 | live |
| 223 | `paper.py` | `KEEP_RATE` 0.07 Solana, 0.15 Robinhood | live, this is the actual fix |

Patches 125 to 209 exist but many have no individual record. Specifically 125 to 136, 138 to 141, 143 to 150, 152 to 153, 155 to 156, 158 to 180, 182 and 184 to 186 are undocumented.

### Three mistakes worth recording, because they are instructive

**1. I optimised the wrong thing, twice.** I estimated the tape fetch was 25% of settlement cost and built patch221 to parallelise it. The profiler said otherwise. In a 45 second pass covering 26 rows:

```
time.sleep inside gmgnlimit.acquire   41.3 s
sqlite3.Connection.execute            18.8 s over 99 calls
_ssl._SSLSocket.read                   0.98 s over 42 calls
```

**The network is 2% of the cost.** Settlement is rate limiter bound, then SQLite bound. Threading could never have helped and patch221 made things strictly worse: a pass that had been pricing 76 rows priced 3, because the workers queued the whole 1,200 row batch, decoded every stored tape up front on the main thread, and ran ahead holding the shared budget.

**2. I gave the rate limit budget away on a bad comparison.** Patch222b compared collector poll rates against 224 and 126 polls per hour, which were a post fix spike, not the long run normal of about 143 and 89. At reserve 3.0 the collectors ran at 178 and 108, above normal, so they were never actually harmed. Meanwhile the final horizon backlog climbed from 292 to 1,071 while I was giving budget away for no reason.

**3. Patch222 and 222c caused a 20 minute box wide collector blackout.** Both chains recorded zero poll cycles between 02:07 and 02:27 UTC on 19 Sep (09:07 to 09:27 Bangkok), consistent with a rate limit IP ban. The limiter carried an `until` stamp inside that window and the only change was the lowered reserve. Reverted to 6.0.

The rule that came out of it, now written into the code comment:

> A slow settlement queue is late data. A ban stops the COLLECTORS, and a missed observation is gone forever.

### The real fix: patch223

Patch199 had sized the sample so that arrivals roughly equalled settlement capacity: 411 arrivals per hour against 436 settles per hour. That looks balanced and is not, because **every row needs at least TWO settles** (the 70 minute stage and the 25 hour stage), plus hourly refreshes in between. Sustainable arrivals are capacity DIVIDED BY TWO.

Measured 19 Sep 2026 05:17 UTC (12:17 Bangkok): about 500 settles per hour of capacity, against 833 arrivals per hour on Solana (already sampled at 0.5, so about 1,666 raw) and 518 per hour on Robinhood at 1.0.

Patch223 set Solana to 0.07 and Robinhood to 0.15, targeting roughly 200 arrivals per hour combined.

Effect, measured 19 Sep 2026 06:04 UTC (13:04 Bangkok):

| window | RH arrivals | SOL arrivals |
|---|---:|---:|
| last 120 minutes | 414 | 885 |
| last 60 minutes | 128 | 247 |
| last 30 minutes | 31 | 28 |
| last 15 minutes | 16 | 21 |

The 15 minute window annualises to about 64 per hour on Robinhood and 84 on Solana, roughly 148 combined, comfortably under the 200 target. Against measured capacity of 209 (RH) plus 401 (SOL) settles in the last hour, that is a large surplus. **The inflow problem is fixed.**

### A second service bug found by the reboot

`trench-sol.service` was never enabled at boot. Twenty five days of uptime had hidden it. The resize reboot exposed it. It is now `enabled --now` and both services show `active, enabled`.

---

## 8. Known data loss, and a correction

### The correction

Two different quantities were being reported as "the deficit" and they must be kept apart.

- **Past 25 hours with no settle yet.** Measured with `COALESCE(settled,0)=0 AND (now - ts) >= 25h`. This was 110 at 04:15 UTC, 523 at 05:17, 1,061 at 06:19 and 3,182 at 13:10 on 19 Sep 2026 (11:15, 12:17, 13:19 and 20:10 Bangkok). **CORRECTED 19 Sep 13:30 UTC: this is NOT a loss counter.** A 1,500 minute kline is a 25 hour window ending now, so for a row of age 25h + d the fetch still covers the needed window minus its first d minutes. The row is settled late with a truncated window, not lost. Measured proof: of 1,902 Solana rows settled to the final horizon in the three hours to 13:10 UTC, every one was in the 25 to 50 hour band and every one came back with candles (average 14.7, zero rows with zero candles).
- **Past 25 hours and not settled to the FINAL horizon.** These rows may have a 70 minute outcome but never got their 25 hour one. This is the larger number, 3,888 on Solana.

An audit written as `WHERE settled = 0` instead of `COALESCE(settled,0) = 0` returns almost nothing, because unsettled rows store NULL. That trap is real and is recorded in section 13, but it was not the source of the 523 figure, which used the correct predicate. An earlier draft of this document compared the 3,888 against the 523 as though they measured the same thing. They do not. That comparison was wrong and is corrected here.

Measured correctly on 19 Sep 2026 06:04 UTC (13:04 Bangkok), restricted to rows that are actually eligible (`tradeable=1 AND ret IS NOT NULL`):

| | eligible total | settled to final | past 25h and not final (**permanent loss**) | never settled, under 25h (**at risk**) | eligible arrivals last 24h |
|---|---:|---:|---:|---:|---:|
| RH `paper` | 15,552 | 12,136 | **1** | 1,262 | 3,359 |
| RH `paperx` | 8,202 | 6,225 | **12** | 666 | 1,928 |
| SOL `paper` | 65,842 | 48,373 | **3,888** | 13,378 | 13,086 |
| SOL `paperx` | 9,297 | 7,543 | **49** | 725 | 1,651 |

**CORRECTED 19 Sep 13:30 UTC.** Two earlier statements in this document were wrong in kind, not merely in size. Neither 523 nor 1,061 was a count of permanently lost rows.

The only unrecoverable state is **age past 50 hours with no final settle**, because at 50 hours the fetch window lies entirely after the needed window and returns nothing usable. Measured at 13:10 UTC on 19 Sep 2026: **zero such rows on either chain.** Nothing is permanently lost yet.

What is real is a race. At 13:10 UTC Solana had 3,187 rows in the 25 to 50 hour band and 10,801 never settled inside the window, so about 13,988 rows still need a final settle. Throughput was 570 per hour and rising (312 per hour six hours earlier), against arrivals of 83 per hour. That clears in roughly 25 hours, which is the width of the cliff itself. Robinhood is already clear: 5 rows never settled, 0 past 25 hours.

So the cost of the pre-fix queue is **truncated windows, not missing outcomes**, unless the drain stalls. The number to watch is rows past 50 hours, not rows past 25 hours.

### The remaining risk, which is NOT yet fixed

There are 13,378 Solana `paper` rows that have never been settled and are still inside the 25 hour window. They are recoverable today. They will not all be recovered, for a structural reason:

The selection query sorts priority class 0 (past 25 hours) ahead of class 1 (never settled), and class 0 is sorted OLDEST first. So settlement grinds through the 3,888 dead rows, whose tape is already gone and whose settlement produces nothing useful, before it reaches a single live row. Meanwhile the oldest of the 13,378 live rows age past 25 hours, lose their tape, and join the dead pile.

At the observed 401 Solana settles per hour against roughly 168 needed for current inflow, the surplus is about 233 per hour and the combined backlog is about 17,266 rows, so a full drain takes roughly 74 hours. Rows older than about 11 hours right now will not be reached in time.

**Proposed fix, not yet applied:** settle the class 0 rows from the observation tape alone, skipping the kline fetch entirely when the entry is more than 25 hours old and `kl.db` holds no cached tape. Those fetches cannot return anything useful, so they are pure waste of the rate limit budget. That would drain 3,888 rows almost immediately and put the entire budget onto the live rows.

I have not applied this because I want it reviewed first. It is the single highest value change available right now.

### The clean cut

Because of the sampling change, the clean data cut for any forward analysis is:

> **19 Sep 2026 05:25 UTC (12:25 Bangkok)**

Data before that cut can be combined with data after it ONLY by filtering the earlier data to the same stable hash threshold (0.07 Solana, 0.15 Robinhood). Raw concatenation would over represent the earlier, more heavily sampled period.

---

## 9. Live state right now

Captured 19 Sep 2026 06:03 to 06:05 UTC (13:03 to 13:05 Bangkok).

```
SETTLE_RESERVE        6.0
TRENCH_KEEP           0.07 (sol) / 0.15 (robinhood)
OBS_PAYLOAD_DAYS      4
PRUNE_CHUNKS          40
settle.sh TOTAL       270
settle.sh FLOOR       40
gmgnlimit RATE        6.0
gmgnlimit CAP         20.0
gmgnlimit FLOOR       4.0

trench.service        active, enabled
trench-sol.service    active, enabled

load average          5.56, 5.97, 6.24
memory                3,915 MB total, 1,971 MB used
disk                  52 GB free of 77 GB (33% used)

RH  settles last hour   209      arrivals last hour   128
SOL settles last hour   401      arrivals last hour   247
```

Relevant cron entries:

```
*/10 * * * *   health.py
1-59/5 * * * * /opt/trench/settle.sh
3-59/5 * * * * /opt/trench/paper.sh
0-59/10 * * * */opt/trench/callbook.sh
17 * * * *     /opt/trench/scoreall.sh
23 * * * *     /opt/trench/rollup.sh
41 7 * * *     /opt/trench/daily.sh
5 */12 * * *   systemctl restart trench.service trench-sol.service   # patch219
```

---

## 10. The three gates, and the open decision

**Gate A** was the retrospective measurement. It is complete. The answer was no: nothing clears $110 on honest fills. Section 5.

**Gate B** is the open decision and it belongs to the operator, not to me. It requires freezing exactly one candidate: one chain, one age route, one entry rule, one market cap range, one exit rule, with the code version recorded. Nothing may move after that.

**Gate C** is the forward test: 14 days of new paper entries plus 24 hours for the last of them to mature, measured against the $110 per $100 hurdle after costs, with no rule changes permitted after results are seen.

Given that Gate A returned no candidate above $110, the honest reading is that Gate B has no obvious candidate to freeze. The best guarded cell in today's recomputation is Solana `paper` with a 50% trailing stop at $90.81, which is a 9% loss per trip.

### Deliberately withheld changes

These are known improvements that were NOT shipped, on the principle that **the entry rule must not move while a ledger is being measured on it**:

- **F3**: the sub 1% box gate should become a warning plus a sample sufficiency rule rather than a hard gate.
- **`dev_under_5`** fires on 81% of rows, which makes it nearly useless as a signal. It needs the same margin treatment that `strong_pad` received.
- **`PAD_MIN=40` should require the pad's LOWER Wilson bound to clear the bar**, not the point estimate. This is the statistically correct form and it is currently wrong.

---

## 11. What I specifically want scrutinised

In descending order of how much it would change things.

1. **Diagnose the `peak_true` blowup in section 6.6.** Given the signature (Solana only, thin tape of 3 to 22 candles, entry caps clustered at $57,000 to $59,000 with liquidity near $22,500, `peak_obs` sane), what is the most likely mechanism? Decimals mismatch, quote asset mismatch (SOL denominated price treated as USD), a pool migration that changes the supply denominator, or something else? What is the cheapest decisive test?

2. **Is `peak_true > 4 x peak_obs` the right guard?** It is inherited from `FIT_SPREAD`. It cannot fire on the 37.4% of Solana rows with no witness. Is there a better internal consistency check that works without a witness, for example rejecting a peak that implies a per candle move above some physical bound, or cross checking against `last_mcap`?

3. **Attack the negative result.** Gate A says nothing clears $110. Section 5b independently agrees using a different ledger. Where could both still be wrong in the SAME direction? Candidate attack surfaces: the fill model charging too much slippage; the closes only rule discarding real intrabar exits that a limit order would have caught; the 5 minute entry cadence systematically entering late; survivorship in which tokens get admitted at all.

4. **Is the sampling defensible?** The stable hash threshold produces nested subsets, which is elegant, but it is a hash of the ADDRESS. Token addresses on Solana are frequently vanity mined (the `pump` suffix is visible in the defect list). If vanity mining correlates with launch style, the hash is not independent of the outcome. Is the first 8 hex characters of SHA1 of an address safe to treat as random with respect to price behaviour? How would you test that?

5. **The settlement priority inversion in section 8.** Class 0 (dead tape, oldest first) starves class 1 (live tape). My proposed fix is to settle dead rows from the observation tape only, with no kline fetch. Is there a better ordering?

6. **Five days of data.** Is any conclusion at all safe from five days spanning a single market regime? What is the minimum honest sample here?

7. **The undocumented `FIT_MIN` knob.** It is in the pipeline and its meaning is unverified. Any Gate A number that depends on it is unconfirmed. How much should that discount the section 5 table?

8. **Gate B.** Given a null Gate A, is running a 14 day forward test on the least bad rule worth the time, or is the correct move to change the search space entirely?

---

## 12. The data

All links are live HTTP and need no login. They are public to anyone with the URL. They contain token addresses and market data only, no credentials and no personal information.

Base: `http://159.223.88.168/hx-c9ois/`

| file | rows | size | link |
|---|---:|---:|---|
| `rh_paper.csv.gz` | 12,136 | 2.24 MB | http://159.223.88.168/hx-c9ois/rh_paper.csv.gz |
| `rh_paperx.csv.gz` | 6,225 | 1.19 MB | http://159.223.88.168/hx-c9ois/rh_paperx.csv.gz |
| `sol_paper.csv.gz` | 48,373 | 8.23 MB | http://159.223.88.168/hx-c9ois/sol_paper.csv.gz |
| `sol_paperx.csv.gz` | 7,543 | 1.56 MB | http://159.223.88.168/hx-c9ois/sol_paperx.csv.gz |

Contents: every row with `settled=2` (fully settled to the 25 hour horizon) and a non null `ret`, ordered by entry time. Columns are the `paper` schema from section 3, plus the six extra `paperx` columns where applicable.

Loading them:

```python
import pandas as pd, json
df = pd.read_csv("sol_paper.csv.gz", low_memory=False)
df["r"] = df["ret"].map(json.loads)
df["hold_1h"] = df["r"].map(lambda d: d["hold_1h"])
```

A reproduction script, `reproduce.py`, is shipped alongside this document. It regenerates every table in section 5b from the four CSVs.

**Not exported, and needed for a full reproduction of the Gate A table:** the per minute kline tapes in `/opt/trench/kl.db` (29.9 MB) and `/opt/trench-sol/kl.db` (73.5 MB). The custom exits A, B, C and D were computed from those candle arrays, not from the stored `ret` blob. Ask and they will be published to the same base URL.

### Caveats on the export

- These are settled rows only. They are therefore conditioned on having survived to settlement, which is a selection. The unsettled and unsettleable rows in section 8 are absent, and section 6.6 shows the settled set is itself contaminated.
- Solana is over represented relative to Robinhood, because Solana settled a far higher share of its arrivals.
- Row counts here (for example SOL `paper` 48,373) are larger than the Gate A run counts (Solana 14,164) because Gate A applied the honest fill filters and the age route restriction. This export is the unfiltered settled set, deliberately, so a reviewer can apply their own filters.

---

## 13. Standing rules and traps for anyone touching this system

1. Never pool the two chains. `cfg.py` and `TRENCH_CHAIN` keep them apart for a reason.
2. Always score `peak / price_at_decision`. Never absolute market cap.
3. A higher `reserve` means LOWER priority in the rate limiter. This reads backwards and has caused mistakes.
4. Never raise `RATE` above 6.0. 14 per second produced a box wide ban.
5. `gmgn.kline` has no start time. Anything older than 25 hours is gone.
6. `settled` is NULL, not 0, for unsettled rows. `WHERE settled=0` silently returns nothing.
7. Settlement only ever touches rows with `tradeable=1 AND ret IS NOT NULL`. Audits that ignore this over count backlog by a factor of three or more.
8. Analysis scripts must be run from `/tmp`, not `/opt/trench`. Something in `/opt/trench` shadows a standard library import and breaks numpy.
9. The bridge to the droplet times out at about 60 seconds regardless of the requested timeout. Long jobs must be launched detached and polled.
10. A slow settlement queue is late data. A rate limit ban stops the collectors, and a missed observation is gone forever. Always protect the collectors first.
11. `/tmp` does not survive a reboot. Anything that matters must live in `/opt` and be version controlled. This rule is written in blood, see section 5a.

---


---

## 15. External review received, and the claims I tested

An external review of this document arrived on 20 Sep 2026. I tested its two most load-bearing claims against the live system rather than accepting or dismissing them. One is wrong. One is right, and the right one is a live bug I had missed.

### 15.1 CLAIM: the 25 hour cliff is not real, because the official client exposes from/to

**TESTED LIVE 20 Sep 2026 13:30 UTC (20:30 Bangkok). The claim is WRONG. The cliff is real.**

Five calls against /v1/market/token_kline for one Solana token with a known 627 candle tape:

```
limit only (baseline)   rows= 627   first 19 Sep 00:55   last 19 Sep 13:03
to= 40h ago             rows=   0   EMPTY
from= and to= window    rows=   0   EMPTY
end= 40h ago            rows= 627   IDENTICAL to baseline, parameter ignored
before= 40h ago         rows= 627   IDENTICAL to baseline, parameter ignored
```

`to` and `from` return an empty list with a success code. `end` and `before` are silently ignored rather than rejected. A client library exposing a parameter is not evidence the endpoint honours it on this route and chain. Section 2 stands unchanged, and so does rule 5 in section 13.

The silent-ignore behaviour of `end` and `before` is the more dangerous of the two: a naive test reads 627 rows back and concludes paging works.

### 15.2 CLAIM: pad_history and dep_history leak future information

**TESTED BY READING THE LIVE CODE. The claim is RIGHT. This is a live bug in the entry path.**

Both functions build a launchpad or deployer track record by selecting `p.ts, p.mcap, p.peak` from `paper`, admitting a row only once `p.ts + 4200` has passed, and then stamping that outcome as available at `p.ts + 4200`.

The ripeness gate controls WHICH rows are admitted. It does not control WHAT VALUE is admitted. `p.peak` is the running lifetime peak, updated by settlement and by every hourly refresh for as long as the row lives. So a sibling launch that entered at T and only doubled at T plus three hours contributes a win that the code dates as knowable at T plus 70 minutes.

Two of the ten boxes rest on this: the launchpad box (`strong_pad`, gated by PAD_MIN 40 and PAD_EDGE 1.25) and the deployer exclusion. Both docstrings claim the record is built strictly without look-ahead, and that nothing there can see a sibling that ran later. **Those claims are false as written.**

Consequences:

- The edges quoted in those docstrings (pads at 2.07x on the chosen 1.25 margin; deployers at 0.21x and 0.26x) are contaminated and cannot be taken at face value.
- This is the sixth defect in this project that resolved ambiguous data in our own favour. The pattern in section 6 holds without exception.
- It fits the review's separate finding that the score barely separates: score 6 or more at 1.08 per cent against a 0.68 per cent control on its Solana low-cap comparison, and WORSE than the control in the 100k to 1M band.

**Not fixed, deliberately.** The entry rule must not move while a ledger is being collected on it. The correct handling is at analysis time: treat the launchpad box and the deployer box as unproven, and rebuild their history offline from dated observations before either is trusted again. Anyone rerunning Gate A or designing Gate B should exclude them or recompute them.

### 15.3 Right, but not yet verified by me

Recorded so it is not lost, each still to be checked:

- A measured entry-anchor failure on `E8ULWeXuVboG9p1jAErAGoFWxu7acCJ5jNSMidxYfba9`: raw payload price 0.0024062678 against a settlement anchor of 0.00000000039903686, a ratio of 6,030,189. That is a specific mechanism for section 6.6 rather than my untested decimals hypothesis, and it is the single most useful lead in the review.
- An entry denominator taken from a candle that closes AFTER the entry, on `3zmVtrnPBeQDAtAQpReN9Cox4xizzpho2ipdoHKRZgGc`.
- Recorded supply changing within one token from 1,000,000,000 to 4,521,816 on `FVGcEyyvnWwrep2Z58xUqnawCjYso4Ab2TrZR4V4cBq9`, which invalidates a fixed price-to-cap multiplier for those rows.
- A 15 Sep copy of `/tmp/pp.py`, which partly answers section 5a. Its `FIT_MIN` default is 3, which answers section 11 item 7: three scale estimates, which can reuse the same candle and are not necessarily independent.
- The wallet collector does not fire until 300 seconds (median 348), and its `RUN_AT` is 100000, not 1000000. So the hypothesis about which wallets buy in the first seconds has never been tested by this system.

### 15.4 The eyes signal must be in the experiment

The review proposes four models to compare: baseline, flow, wallets, combined. That set is missing something this system already collects on every poll.

`obs.eye` and `paper.eye` hold the provider attention count. It is present from the first observation, long before the wallet roster exists at 300 seconds, and it is upstream of buying rather than derived from it. It is therefore one of the very few candidate signals that is actually available in the window the review says matters, seconds 30 to 180.

It must be tested on its own terms, with the same discipline as everything else:

- Test the SLOPE, not the level. A high count on a coin that has been visible for two minutes is not the same event as the same count reached in twenty seconds.
- `smart` and `call` sit beside it in the same payload and may carry the same information. Test whether eyes adds anything once flow is in the model, exactly as the review asks of wallets.
- Watch for the silent zero. An absent `eye` field reads as 0.0 and is indistinguishable from a measured zero. Section 1 of this document exists because of that failure mode.
- Coverage changed over time on other fields in this payload (net-buy coverage moved from 42.4 per cent to 100 per cent between the review development and diagnostic windows). Check `eye` coverage by date before using it, or missingness becomes a calendar predictor.

So the comparison set is five, not four: baseline, flow, eyes, wallets, combined. The question for each added block is the same: does it improve the previous model on later unseen launches. If it does not, drop it.

---

## 14. Version

Price Predict handover, version 1.2. Generated 19 Sep 2026 06:10 UTC. Section 8 corrected 19 Sep 13:30 UTC. Section 15 added 20 Sep 13:40 UTC (20:40 Bangkok) after an external review was tested.

Latest version of this document, always: http://159.223.88.168/hx-c9ois/PRICE_PREDICT_HANDOVER.md The data it describes is published at the base URL `http://159.223.88.168/hx-c9ois/` and can be fetched directly, with no login:

- http://159.223.88.168/hx-c9ois/rh_paper.csv.gz
- http://159.223.88.168/hx-c9ois/rh_paperx.csv.gz
- http://159.223.88.168/hx-c9ois/sol_paper.csv.gz
- http://159.223.88.168/hx-c9ois/sol_paperx.csv.gz

`reproduce.py` is shipped alongside this document in chat.

If a newer revision of this document is generated, it replaces this one and carries a higher version number in this section.
