Caverio trader companion, overnight conceptual design, 2026-09-26. Worker T2 (Opus 5.5), started 00:37 Beirut.
Everything described in §1 is the experimentation stack, per Thomas's 00:28 decision. It is measured here so the build stack can be designed against real numbers, not so it can be kept by default.
How to read the numbers. Every number from our own machine comes from a script in scripts/T2-* with its output in data/T2-*, run between 00:37 and 00:55 on 2026-09-26 (grade A, ours). Web or vendor facts carry a grade: A primary source fetched and cited, B credible secondary, C my inference or an estimate. Estimates are labelled as estimates.
| Script | What it measured | Output |
|---|---|---|
scripts/T2-procs.sh |
process inventory (pid, uptime, RSS, lifetime CPU) | data/T2-procs-0037.tsv |
scripts/T2-unit-sample.py 300 |
CPU over a 300 s window per systemd unit (CPUUsageNSec delta) and per non-unit process; memory | data/T2-unit-sample.tsv |
scripts/T2-net.py 300 |
per-process network bytes from ss -tinp tcp_info counters over 302 s, loopback excluded; interface totals |
data/T2-net.tsv |
scripts/T2-disk.sh |
bytes, files and newest mtime per entry of signal-room/data/ |
data/T2-disk-20260926-0038.tsv |
scripts/T2-requests.py |
provider calls per day from broker.log, tape round failures by reason, tape state, holdings counters |
data/T2-requests.txt |
scripts/T2-volumes.py |
rows per day: callers, Fomo events, tape trades, deep-paper observations, events.sqlite | data/T2-volumes.txt |
scripts/T2-llm-grep.sh, scripts/T2-llm-ledger.py |
every model call in the stack, and the one token ledger that exists | data/T2-llm-grep.txt, data/T2-llm-ledger.txt |
scripts/T2-units-cron.sh |
crontab, user units, timers (read-only) | data/T2-units-cron.txt |
scripts/T2-budget.py |
the 50 / 200 / 500 token budget model in §5, all constants named with their basis | data/T2-budget.txt |
Measurement caveat for tonight. Nine other overnight workers (claude -p) were running during the samples. They show up in the openclaw-gateway CPU (164% of one core) and in the interface totals (25.7 GB/day down, 11.9 GB/day up if extrapolated). They are not the stack. Per-process rows below exclude them. For a clean baseline, the 09-25 system audit's post-decommission reading is the reference: download 4.1–5.6 GB/day, upload 1.9–3.1 GB/day, load about 1.7 (projects/system-audit-2026-09-25/AUDIT.md).
The crypto stack was cut to "option B, research-only" on 2026-09-25 at 13:10 (Thomas: "B, data is important"). Fifteen services were stopped and disabled, including the situations writer, patterns, telemetry, paper-tiered, case-builder, admin-status, thesis-enrich, gate and the cloudflared tunnel. Fifteen raw collectors kept running. signal-room-v2 (the server) was restarted at 17:09 because two kept collectors depend on it. So today's picture has three layers:
/api/feed, holdings and the BSC tape read the feed.json it writes),Host: 28 cores, 64 GB RAM, 915 GB disk with 567 GB free. signal-room/data/ is 15.6 GB (was 13 GB at the 09-25 00:45 audit).
CPU is the share of one core averaged over the 300 s window. RSS is from /proc. Requests per day are from the workers' own counters where they exist. Disk growth is the 24 h delta against the 09-25 audit sizes (audit table du -h values against tonight's du -sh), so it is ±5% resolution. "Layer" is the product layer from the mandate: L1 money brief, L2 discovery and situations, L3 zones.
| Component (unit) | What it does | Cadence | RSS | CPU | Requests/day (source) | Disk growth/day | Layer | Verdict and why |
|---|---|---|---|---|---|---|---|---|
caverio-price-paths (workers/price_paths.py --feed) |
re-measures price paths for ~9,880 subjects | 60 s | 232 MB | 52% | 0 external; 1,440 local GETs of /api/feed |
price-paths.jsonl 6.4 → 6.7 GB (+0.3 GB) | L3 research | Rebuild. It re-parses its own 7.09 GB append-only ledger every cycle (samples_read()), so its CPU grows with its file. Latest-row-per-subject is ~46 MB. The product needs forward returns per situation, computed once from the tape and candles, not a re-scan. |
signal-room-v2 (server.py :3045) |
legacy read model + API, kept only as a dependency | on request, rebuilds room | 502 MB | 45% | 0 external | room.json 99.6 MB rewritten, feed.json 7.4 MB | L2 (dead UI) | Retire after price_paths, holdings and the BSC tape stop depending on feed.json. It spends half a core building a 100 MB room nobody views. |
signal-room-v2-market (workers/market-refresh.mjs --loop 60) |
DexScreener price poll via broker; writes ticks, deep-paper observations, market-cache | 60 s | 105 MB | 12% | 3,594 DexScreener calls (batches of 30) | deep-paper 314 → 435 MB (+0.12 GB), ticks 41 → 44 MB | L2 price | Keep with fix, then fold into the price collector. Fixes 1–3 of the audit (raw sidecar, source + providerAt, unchanged flag). 33% of its prints are exact repeats. |
signal-room-v2-broker (workers/market-broker.mjs :3046) |
single client for every market provider, budget pacing | per request | 111 MB | 0.0% | DexScreener 3,594, DexPaprika 1,451, CoinGecko 1,179, GeckoTerminal 4 | broker.log ~1 MB | L1/L2 | Keep. The one good pattern for provider budgets (credit pacing, 429 accounting). Becomes the fetch layer for all HTTP collectors. |
caverio-chain-tape-bsc (workers/chain-tape.mjs --chain bsc) |
BSC swap tape per live pool via eth_getLogs on public RPC | continuous | 610 MB | 1.3% | ~128,000 failed rounds/day (99.5k "archive requests require a personal token", 28.9k "ranges over 10000 blocks") | trades-bsc 998 → 1,022 MB (+24 MB, collapsing: 155k rows on 09-20, 22k on 09-25) | L2 flow | Rebuild the fetch strategy, keep the schema. 57 of 64 pools carry a lastError; pools that fell out of publicnode's head window never recover and retry forever. The log moves, so the stall watchdog sees it as healthy. See §6. |
caverio-chain-tape-solana (--chain solana) |
Solana swap tape per pool via signatures + getTransaction | 5 s live poll, 150 s round deadline | 176 MB | 0.4% | Helius 20,000 / 20,000 used (state file); now public fallback | trades-solana 72 → 107 MB (+35 MB) | L2 flow | Rebuild. Hung 8 days to 09-25 00:22, stalled again 07:40–12:09 (watchdog), and 280 rounds logged "N of M left unfetched at the 150 s deadline" (latest: 2,986 of 3,153). Per-transaction fetch cannot scale (§5). Download 848 MB/day for ~26k rows (32 KB per stored row). |
signal-room-v2-onchain (lib/onchain-candles/ingest.mjs) |
Robinhood Chain swaps → trades + 1 m candles | continuous | 454 MB | 2.3% | public RH RPC; download 746 MB/day | candles-onchain 107 → 121 MB (+14 MB) | L2 flow (RH only) | Keep with fix until the RH decision (D4). Merge into the one EVM tape collector. |
signal-room-v2-holdings (workers/holdings.mjs --loop 120) |
early-buyer balance coverage per open situation | 120 s | 1,218 MB (heap 940 MB, cap 1,536) | 1.3% | ~2–3 RPC calls per pass via Multicall3 (~2,000/day) | holdings 50 → 51 MB | L2 holders | Rebuild. Largest RSS in the stack for 23 situations; 46% of files are "chain has no tape" stubs (audit). Holder concentration is a derived field over the tape, not a service. |
signal-room-v2-candles (workers/candles-service.mjs) |
OHLCV cache + outcome marks | 300 s | 110 MB | 0.5% | GeckoTerminal/CoinGecko via broker | ohlcv ~6 MB, outcomes flat | L2/L3 | Keep with fix (providerAt on outcomes, audit fix 2). Becomes the candle fetcher behind the broker. |
signal-room-v2-discovery (workers/discovery.mjs --loop 60) |
screens caller posts into discovery rows | 60 s | 66 MB | 0.0% | via broker | discovery.jsonl +small | L2 discovery | Keep the logic, rebuild as a derivation over the event store. |
Callers cron (watchtower/callers.mjs, */5) |
Telegram caller channels via public t.me/s/ previews, 5 channels |
5 min | n/a (cron) | n/a | ~288 page fetches/day | callers.jsonl: 28–127 msgs/day captured | L2 discovery | Rebuild. Five channels, formatted calls only; the mandate asks for whole chats and many more callers. Needs a real Telegram client session (T4 decides the source). |
Fomo watcher (watch-tracked-accounts.mjs, cron watchdog) |
Fomo websocket alerts for 119 traders | streaming | 90 MB | 0.0% | 1 websocket; 1,000–2,000 events/day | raw-events-v2.jsonl ~1 MB/day | L2 discovery | Keep. Cheapest high-signal source we have. Move under a real unit with a heartbeat (§6). |
crypto-experiment-a-collector / -marker |
raw Fomo collector + marker (research) | loop | 53 / 273 MB | 0.0% / 7.8% | Fomo REST (credits) | research dir | research | Keep until its experiment reports, then retire. The marker costs 8% of a core. |
caverio-social-sweep (social-sweep/collector.mjs --no-x) |
per live situation: X (dark) + public t.me previews | 10 min | 126 MB | 0.1% | X 0 (dark since 09-18); t.me ~91 MB/day down | social-sweep 24 → 24 MB | L2 social | Retire into the new social collector (T4). |
caverio-social-reader (--provider none) |
one counted-fact sentence per situation | 10 min | 98 MB | 0.2% | 0 LLM calls since 09-20 (template only) | social-read ~6 MB | L2 social | Retire. Its ledger is our only measured LLM cost basis (§5): 430 input / 55 output tokens per rephrase call, 2,601 calls and $1.76 on its busiest day (09-17). |
crypto-watchtower (watchtower.mjs) + smartmoney cron |
wallet watch, module A monitor + paper | loop / 5 min | 202 MB | 0.6% | download 208 MB/day, upload 608 MB/day | watchtower data | L2 wallets | Keep the wallet list, rebuild the collector onto the tape (a wallet buy is a tape row with a known actor). The 608 MB/day upload needs a look; I did not attribute it. |
signal-room-proof-daytime (+ proof_supervisor.py, onchain-bridge/proof-v1/run.mjs) |
funded-proof on-chain observer (research) | 600 s bounded children | 148 MB | 1.9% | download ~1 GB/day | proof runtime | research | Retire at the end of the proof experiment; its useful output (linked receipt flow) is a tape derivation. |
| Timers: store-sync (60 s), catcher-publish (30 s), proof-detector (15 s), source-sentinel (5 min) | JSONL → events.sqlite mirror; catcher annotations; detector; source cadence sentinel | timers | oneshot | 2.3 s and 0.3 s CPU per run | 0 external | events.sqlite 372 → 412 MB (+40 MB) | L2 | store-sync: keep the pattern, replace the mirror with direct writes. Others: retire with the ladder. |
Crash loops: crypto-fast-sources, crypto-upstream-paper |
research collectors | restart every 15–30 s since at least 09-25 13:00 | n/a | n/a | n/a | journal spam | none | Retire now (one is status=200/CHDIR, a missing directory). Pure waste and journal noise. |
Five wrangler pages deploy processes |
stuck Cloudflare deploys, 26–50 h old | hung | ~180 MB each, ~900 MB total | 0% | n/a | n/a | none | Kill (Rook, not this lane). Memory leak by neglect. |
Totals for the stack (excluding tonight's agents): about 1.3 cores of CPU, of which 0.97 is price_paths plus the server, i.e. two processes doing re-computation; about 4.3 GB RSS; download about 3.3 GB/day attributable per process (a floor: short-lived sockets are missed), matching the audit's 4.1–5.6 GB/day total; disk growth about 0.55 GB/day with the ladder writers stopped, of which 0.3 GB is the price-paths re-measurement ledger. External requests: about 6,200/day to market APIs through the broker, plus about 128,000/day of failing BSC RPC calls.
data/).flowchart LR
subgraph Ext["External sources"]
CH["Chains: Solana, BSC, Base, (Robinhood)"]
MK["Market APIs: DexScreener, CoinGecko, DexPaprika, GeckoTerminal"]
MAC["Macro: stablecoin supply, CEX/DEX volume, BTC dominance (T3)"]
SOC["Social: Telegram callers + chats, X accounts (T4)"]
FOMO["Fomo: trader alerts + theses"]
LLM["LLM API: Haiku-class extraction, Sonnet-class stance"]
end
subgraph Box["Caverio backend (one box to start)"]
COL["Collectors (supervised, heartbeat)"]
STORE[("Event store + tape store + raw sidecar")]
DER["Derivers (rebuildable)"]
RM[("Read models per page")]
API["API (read-only)"]
end
subgraph Users["Beta users"]
WEB["Web app: market brief, discovery list, situation page"]
end
CH --> COL
MK --> COL
MAC --> COL
SOC --> COL
FOMO --> COL
COL --> STORE --> DER --> RM --> API --> WEB
DER <-->|budgeted calls| LLM
OPS["Ops: supervisor, gap ledger, cost ledger"] -.-> COL
OPS -.-> API
flowchart TB
subgraph Capture["1 Capture (raw, append-only, never rewritten)"]
T1["tape collectors<br/>swap/trade rows"]
P1["price collector<br/>raw provider payload + normalised print"]
A1["attention collectors<br/>messages, posts, Fomo alerts"]
M1["macro collector<br/>daily/hourly series"]
end
subgraph Norm["2 Normalise"]
EV[("events: source, sourceId, raw sha256,<br/>receivedAt, availableAt, providerAt, quality")]
TP[("tape: chain, block/slot, tx, logIndex,<br/>pool, side, amounts, usd, quoteSource")]
PR[("prints: token, price, liq, mcap,<br/>provider, providerAt, unchanged, phantom")]
end
subgraph Derive["3 Derive (pure functions of 2, rebuildable)"]
C1["1 m candles per pool (from tape)"]
F1["derived fields per token per minute:<br/>net flow, buyers, holder concentration, volume vs band"]
X1["mentions + stance per caller per token<br/>(LLM, versioned prompts)"]
MB["market brief series<br/>(chain, band, BTC vs alts)"]
GAP["gap ledger per collector per scope"]
end
subgraph Situ["4 Situations"]
S1[("situation store: token, openedAt, trigger events,<br/>lights (categories lit), rank inputs")]
end
subgraph Read["5 Read models (one document per view)"]
R1["brief.json per horizon"]
R2["discovery list"]
R3["situation page doc per token"]
end
T1 --> TP --> C1 --> F1
P1 --> PR --> F1
A1 --> EV --> X1 --> F1
M1 --> EV --> MB --> R1
F1 --> S1
EV --> S1
GAP --> R3
S1 --> R2
S1 --> R3
F1 --> R3
C1 --> R3
R1 --> API2["API"]
R2 --> API2
R3 --> API2 --> PAGE["page"]
Collectors. One small process per source family, each doing only fetch, stamp, append. No collector computes anything a deriver could compute later. Four families:
- Tape. One EVM tape collector (BSC, Base, optionally Robinhood) using one eth_getLogs per block window per chain with an address array for all tracked pools, not a request per pool. One Solana tape collector on a subscription (WSS logsSubscribe or a gRPC stream filtered to tracked pools), not per-transaction getTransaction. Both write the tape standard below.
- Price. One price collector behind the broker, batched 30 tokens per DexScreener call, writing the raw payload to a sidecar and a normalised print with providerAt, unchanged and a phantom flag. CoinGecko/DexPaprika/GeckoTerminal only for candles and backfill. Our own tape candles become the primary price for tokens we tape; DexScreener becomes the fallback and cross-check. This also answers the rights critic: the displayed price is our tape, not a third-party print (T1 owns the rights verdict).
- Attention. Telegram (callers and whole chats, T4), X (when a source exists), Fomo websocket and theses. Every message stored raw once, keyed by source and sourceId.
- Macro. Low-frequency series for the market brief (T3 names them): stablecoin supply by chain, DEX volume by chain and platform, BTC dominance, market-cap band aggregates. Hourly or daily; a few hundred requests a day.
The event standard (applied everywhere). The events.sqlite pattern that already exists, made mandatory: source, sourceId, originalSourceId, raw blob (or a pointer into the raw sidecar), sha256 of raw, receivedAt (our clock), providerAt (their clock, null when they do not give one, never copied from ours), availableAt (when it became usable to a user, for honest replay), quality (source_observation, legacy_unverified, proof_recorded_not_reverified, plus new phantom, unchanged). Unique on (source, sourceId), content-addressed on sha256. Today availableAt is null on 91% of rows; in the build it is set at write time or the write fails.
The tape standard. The trades-bsc schema, which the audit rates full provenance: chain, txHash / signature, block / slot, logIndex / instruction index, blockTime, receivedAt, availableAt, pool, side, token amounts, usd, quoteUsdSource, tsBasis=block, schema. Keyed on (chain, txHash, logIndex). Add one thing it lacks: a coverage record per pool per window (from_block, to_block, status ok / gap / not_found / archive_unavailable). Without coverage rows a missing trade and a quiet minute look identical.
Derived-field store. Everything computed from capture: 1 m candles from tape, per-token per-minute fields (net buy USD, unique buyers, new holders, top-10 share, volume relative to peers in the same mcap band), attention fields (mentions per hour, distinct callers, stance balance, flips), brief series. Each row carries derivedFrom (input ranges and their gap status) and deriverVersion. Dropping and rebuilding any derived table from capture is a supported operation, and a nightly job proves it on one day.
Situation store. One row per token episode (situationId, token, openedAt, openedBy event ids, closedAt), plus an append-only table of light changes (category lit or dimmed, at, evidence event ids). The audit's rule becomes a constraint: no light change without evidence ids. T5 defines the categories and ranking; this module only stores them.
Read models. One pre-built JSON document per view: brief/{today,week,month}.json, discovery.json, situation/{token}.json. Rebuilt by a deriver on change, not on request. The API never computes; it serves documents. This replaces room.json (100 MB, rebuilt on request by the server) with small documents built once.
API. Read-only HTTP, auth by beta account, rate-limited per user, cacheable (ETag on each document). Also serves /status (collector heartbeats and gaps) and the internal query layer (§4.4).
Page. Static front end (Cloudflare Pages is already in use) reading the API. Wireframes are T8's.
| Today | Target module | Carry over | Leave behind |
|---|---|---|---|
events.sqlite, model/event_store.py, workers/store_sync.py |
event standard + event store | schema, dedupe on sha256, quality classes | the JSONL-then-mirror double write; legacy_unverified rows stay marked |
chain-tape.mjs + lib/chain-tape/* (bsc, solana), lib/onchain-candles (RH) |
tape collectors | row schema, pool resolution, quote sources | per-pool polling, per-tx Solana fetch, public-archive dependence |
market-refresh.mjs, market-broker.mjs, lib/candle-sources.mjs |
price collector + broker | broker pacing and budget accounting, the provider ring | ticks/ and deep-paper as separate writers of the same print |
candles-service.mjs, ohlcv/, candles-onchain, candles-bsc |
derived candles | onchain candle builder (rebuild.mjs) | four candle stores with two schemas |
watch-tracked-accounts.mjs, callers.mjs, social-sweep, thesis-enrich |
attention collectors | Fomo websocket, roster files, thesis budget rule | t.me preview scraping as the only Telegram path |
holdings.mjs, telemetry.mjs, case-builder.mjs, lib/case-d8-fields.mjs |
derived fields | field definitions worth keeping (T5 picks) | services that hold 1.2 GB to compute a field |
situations.py, model/states.json, alerts, patterns |
situation store | evidence-id discipline | the Now / Situation / People ladder (out, per Thomas) |
server.py, room.json, feed.json, gate |
API + read models | gate device-cookie idea | read model built on request |
price_paths.py, deep-paper, paper-tiered, strike |
research bench (off the product path) | the replay tool and the forgiving-win measure (T6) | re-measurement ledger as a live service |
source_sentinel.py, chain-tape-stall-watchdog.sh |
supervisor + gap ledger | per-source cadence policy, local outbox | log-mtime liveness |
Thomas's rule of 2026-09-19 governs: Caverio is first a database; keep any received data that could be useful, discard only if it hurts. "Hurts" has three meanings here: it costs more than it could be worth, we have no right to hold it, or it creates a privacy or legal exposure.
| Class | What | Why keep | Retention |
|---|---|---|---|
| Tape | every swap on every tracked pool, from pool open, with coverage records | it is the product's own truth and cannot be bought back later at our price | forever, compressed after day close |
| Raw market payloads | the provider response behind each price print (sidecar) | the only way to tell a provider's bad print from our handling (audit finding 2) | 90 days raw, then keep only payloads referenced by a flagged print or a situation event |
| Messages | every Telegram message from monitored callers and chats, every X post fetched, every Fomo alert and thesis, raw text + ids + author + timestamps | stance, flips and caller track records are rebuilt from these; a message is cheap (0.5 KB) | forever (text only; no media) |
| Macro series | provider series as fetched | brief history and backtests of "how flows moved markets" | forever |
| Wallet labels and rosters | every version of every list | the track record of a list is part of the record | forever, versioned |
| LLM calls | prompt hash, model, prompt version, input ids, output, tokens, cost | reproducibility and the cost ledger | forever for outputs, 30 days for full prompts |
Candles from tape, per-minute derived fields, holder concentration, band membership, attention aggregates, stance labels (the LLM output is stored, but a new prompt version re-derives), situations and lights, read models, forward-return marks. Retention: current version kept; previous versions kept 30 days then dropped, because they can be rebuilt. The rebuild must be tested (§2.3), or "rebuildable" is a claim.
| Not captured | Why |
|---|---|
| Full-chain firehose (every swap on every pool of a chain) | cost: at Solana meme volume it is tens of GB/day and a paid stream tier; we tape tracked pools plus a discovery ring, and use aggregate DEX volume APIs for the chain-level brief (C) |
| Media from Telegram / X (images, video) | cost and rights; text and links are enough for mention and stance |
| Private user data of beta testers beyond account and feedback | privacy; the feedback-for-access programme (T9) needs email, usage events and feedback, nothing else |
| Re-measurement ledgers (price-paths style) | a re-measurement is a derivation; store the latest per key and rebuild on demand |
| Private Telegram groups we joined under a personal account, for display | rights and trust; may be read for stance only if T4 and T1 clear it, never quoted raw on the page |
| Stale repeats as new observations | stored with unchanged: true, never counted as an observation |
Capture: forever (compressed), except raw market payloads 90 days selective. Derived: current plus 30 days. Read models: current only. Logs: 14 days rotated (today they are not rotated at all). Backups: capture only; derived is rebuilt, which removes 85% of today's 15.6 GB from any backup.
| Class | Store | Keys | Partitioning | Why |
|---|---|---|---|---|
| Tape rows | Parquet (zstd) for closed days, append-only JSONL or SQLite for the open day | (chain, tx, logIndex) |
tape/chain=<c>/day=<YYYY-MM-DD>/ |
columnar scans over millions of rows; 6–10x smaller (C); DuckDB reads it in place |
| Coverage records | same partition as tape | (chain, pool, from_block) |
by day and chain | joins with the tape for gap accounting |
| Events (messages, alerts, prints, macro) | SQLite with WAL, one file per month | (source, sourceId), unique sha256 |
by month | the existing standard; single-writer per file is enough at our volumes |
| Raw payload sidecar | content-addressed zstd files | sha256 |
raw/<source>/<day>/ |
dedupes naturally; pointer stored on the event |
| Derived fields, candles | Parquet per day, or DuckDB tables | (token, minute) |
by day | rebuilt, so any format works; Parquet keeps it tool-neutral |
| Situations, lights, accounts, feedback | Postgres or SQLite (see 4.3) | situationId; (situationId, at) |
none | small, relational, transactional, needed by the API |
| Read models | JSON files (or KV) | view + token | none | served as-is with ETags |
token = chain:address everywhere (the feed already uses it). pool = chain:poolAddress. Every derived row points to its inputs by (store, key range) and records the gap status of those inputs. A situation page is always addressable by token plus asOf, which is what makes an honest replay possible.
The 40-entry data directory is the problem the audit named in fix 9: four candle stores in two schemas, two writers of the same print, a 7 GB file that one endpoint scans. There are three options:
| Option | Gains | Costs | Verdict |
|---|---|---|---|
| A. Postgres for everything (with TimescaleDB for tape) | one query surface, transactions, concurrent writers, standard ops tooling, easy to host later | tape at 500 tokens is ~1M rows/day; fine for Postgres but the box then needs vacuum/index tuning; raw blobs bloat it; another daemon to supervise and back up | Overkill for capture on one box; right for the small relational part if we move off the box |
| B. DuckDB over Parquet + SQLite for events + small relational DB | zero daemons; Parquet files are the backup and the interchange; DuckDB scans days of tape in milliseconds; SQLite is what events.sqlite already is | DuckDB is single-writer; needs a clear writer per file and a compaction job at day close; less familiar for concurrent API reads (the API reads read models, so this is acceptable) | Recommended to start. It matches the workload: append-heavy capture, analytical derivation, document-serving API |
| C. Keep the directory, add a query library (what Bolo's item 9 is doing) | no migration | keeps four candle schemas and double writers; the query library has to know every quirk | Good as a bridge for the research bench, not the product |
Recommendation: B for capture and derivation, SQLite (or a small Postgres) for situations, accounts and feedback, with the relational part kept behind one module so it can move to Postgres when there is more than one box. Decision D3.
One Python (or Node) module, caverio.query, with DuckDB underneath, exposing the questions the product and research actually ask, for example:
- tape(token, t1, t2) with coverage status;
- candles(token, t1, t2, source=tape|provider);
- fields(token, t1, t2, names);
- mentions(token, t1, t2) with caller, stance, prompt version;
- crossings(chain, band, t1, t2) with post-path at honest prices (audit fix 9's example);
- gaps(collector, scope, t1, t2).
The API and the research bench both call it; nothing else opens the files. That is what lets storage change later without touching the page.
The situation deriver writes situation/{token}.json whenever an input for that token changes (debounced to 15–60 s). It contains the header numbers (price, mcap, liquidity, volume, flow), the chart series (1 m candles for 24 h, 5 m for 7 d), the lights with their evidence ids, the attention block (mentions, callers, stance balance and flips, peers in band), and the observation gaps for every input over the displayed window. The API serves that file with an ETag; the page does one GET and optionally subscribes to a small delta stream. Target size under 200 KB per token (C). At 500 tokens that is 100 MB of read models, rebuilt incrementally.
"Tracked" means a token with a live situation: its pools taped, price polled, attention read. The model is scripts/T2-budget.py; every constant is named there with its basis. Numbers marked MEASURED come from tonight's scripts; the rest are estimates (C).
| Assumption | Value | Basis |
|---|---|---|
| Price poll | every 60 s, 30 tokens per DexScreener call | MEASURED: 3,594 calls/day for ~100 feed tokens today |
| Candle / backfill calls | 26 per token per day | MEASURED: (1,179 CoinGecko + 1,451 DexPaprika) / ~100 tokens |
| Trades per tracked token | 2,000 per day average (heavy tail: hot memes 20k+) | MEASURED: BSC ~82k rows/day over ~40 live pools on 09-22/23; Solana ~1,300/pool/day on 09-25 |
| Stored bytes per trade row | 1.2 KB | MEASURED: 1,122 B BSC, 1,297 B Solana |
| Solana share of tracked tokens | 50% | C (meme flow; higher in the $1M+ band) |
| Solana wire bytes per trade | 32 KB with today's per-tx fetch; ~3 KB on a filtered stream | MEASURED 848 MB/day for ~26k rows; stream C |
| EVM tape requests | one address-array eth_getLogs per 3 s per chain, 2 chains |
design target; flat in token count |
| Compression | 8x zstd on JSONL tapes | C |
| Telegram channels monitored | 40 / 100 / 200 | C, scaled with discovery breadth; T4 sets the list |
| Messages per channel per day | 120 blended | C: call channels 20–50/day, chats 300+/day; today's 5 channels give 28–127/day total |
| X posts per tracked token per day | 30 from curated accounts | C; X access is dark today |
| Extraction (Haiku-class) | 20 messages per call; 1,500-token cached system prompt; 120 input + 40 output tokens per message | C, consistent with the measured 430 in / 55 out per social-reader call for a single-token prompt |
| Stance (Sonnet-class) | on the 20% of messages that name a tracked token; 1,000 input (last 5 messages by that caller + token card) + 120 output | C |
| Model prices per 1M tokens | Haiku 4.5 $1 in / $5 out / $0.10 cache read; Sonnet 5 $2 in / $10 out | B: claude-api skill model table, cached 2026-06-24 |
| Metric | 50 tokens | 200 tokens | 500 tokens |
|---|---|---|---|
| Requests/day: price (DexScreener, batched) | 2,400 | 9,600 | 24,000 |
| Requests/day: candles and backfill | 1,300 | 5,200 | 13,000 |
| Requests/day: EVM getLogs (flat) | 57,600 | 57,600 | 57,600 |
| Requests/day: Solana, today's per-tx design | 55,000 | 220,000 | 550,000 |
| Requests/day: Solana, stream design | ~100 (reconnects) | ~100 | ~100 |
| Messages/day ingested (Telegram + X) | 6,300 | 18,000 | 39,000 |
| Bandwidth down GB/day, stream design | 0.4 | 1.5 | 3.8 |
| Bandwidth down GB/day, today's design | 1.9 | 7.3 | 18.3 |
| Disk raw GB/day before compression | 0.30 | 1.2 | 3.0 |
| Disk GB/month capture, compressed | 1.1 | 4.4 | 11 |
| Disk GB/month derived (rebuildable) | 1.3 | 5.2 | 13 |
| CPU cores (estimate) | 0.65 | 1.1 | 2.0 |
| RAM GB (estimate) | 1.8 | 2.7 | 4.5 |
| LLM tokens/day, extraction (Haiku-class) | 1.5 M | 4.2 M | 9.2 M |
| LLM tokens/day, stance (Sonnet-class) | 1.4 M | 4.0 M | 8.7 M |
| LLM $/month, extraction | $62 | $177 | $383 |
| LLM $/month, stance | $121 | $346 | $749 |
CPU and RAM assume the target design (collectors do only fetch and append; derivation incremental; API serves documents). Today's stack uses 1.3 cores for about 100 tokens, three quarters of it on re-reading, so the target is cheaper at 200 tokens than today is at 100. The box (28 cores, 64 GB, 567 GB free) carries 500 tokens with room; hardware is not a constraint at any of these sizes.
| Line | 50 | 200 | 500 | Basis |
|---|---|---|---|---|
| LLM (extraction + stance) | $183 | $523 | $1,132 | table above |
| Solana data | $0–49 | $49–99 | $99–499 | today's per-tx design needs 1.7 / 6.6 / 16.5 M calls a month: Helius Developer $49 (10M credits) covers 50 and 200, Business $499 needed at 500 (B, PROVIDERS.md 2026-09-16, Helius billing page). A filtered stream: Sodae free 5 GB/30 d covers 50; Chainstack $49/stream, Shyft ~$99, QuickNode $299 (B, same doc) |
| EVM RPC (BSC, Base) | $0–49 | $0–49 | $49 | today's public endpoints fail archive reads; NodeReal free key or one paid key (C) |
| Market data | current plans | current plans | current plans | CoinGecko Basic, DexScreener free; T1 owns the rights and price verdict |
| Fomo | $49.99 | $49.99 | $49.99 | Starter plan, expires 2026-10-10 (A, /v2/me, PROVIDERS.md) |
| Telegram / X access | T4 / T1 | T4 / T1 | T4 / T1 | X paid API tiers are the open line; not priced here |
| Hosting | $0 marginal | $0 | $0 | Bot-PC; a second box or VPS for redundancy is a D8 choice |
| Total excluding X | ~$230–330 | ~$620–720 | ~$1,330–1,730 |
lib/social-read/model.mjs already has, falling back to "stance unavailable" (printed, not guessed); (e) nightly batch re-scoring at the batch discount for anything not needed live.Restart=always fires only on exit.systemctl --user returned "unknown" from 11:40 until patched); it restarted the unit at 12:09.4h horizon, 09-17) would have killed price_paths at its next restart.All five are the same failure: we measure whether a process runs, not whether data arrives.
Restart=always, RestartSec with backoff, MemoryMax and CPUQuota, WatchdogSec, and linger enabled so it survives reboot. No cron-launched long-runners, no transient units, no nohup.heartbeat/<collector>.json every cycle: at, cursor (block, slot, message id, or print time per scope), rowsSinceLast, errorsSinceLast, budgetRemaining. It also calls sd_notify WATCHDOG=1 only when the cursor advanced, so a hung promise gets killed by systemd itself.providerAt; for a channel, the channel's own posting rate. Thresholds per class: tape lag over 5 min, price older than 3 polls, a Telegram channel silent for 3x its median gap. Breach state is written, then acted on.degraded, alert once, and switch to the fallback provider if one is configured. A failing scope (one pool, one channel) is quarantined without taking the collector down. Exponential backoff on provider errors, with the reason class stored (archive_unavailable, rate_limited, credits_exhausted, not_found).Requires=/After= for real dependencies, and the target design removes the ones that should not exist (collectors must not depend on the API server).Every collector writes coverage rows per scope: (collector, scope, from, to, status, reason). Gaps are first-class data, stored in the event store, and joined into every derived field and read model. The situation page prints them in place: "Observation gap 07:39 to 12:09: Solana tape stalled; flow and buyers for this window are missing." Derived numbers that span a gap carry partial: true and are never presented as complete. Backfill closes a gap and writes a new coverage row with status: backfilled and the backfill time; the original gap stays in the history, so replays remain honest about what a user could have seen at the time (the availableAt rule).
The business plan's gate (ops critic, 09-25) should be measured from coverage rows, per collector class, daily:
| Metric | Definition | Target |
|---|---|---|
| Observed-time share | share of minutes in each tracked token's window covered by the tape with status ok | ≥ 98% per chain per day |
| Max single gap | longest gap per collector per day | ≤ 15 min |
| Price freshness | p95 of now minus latest providerAt at page-build time |
≤ 3 min |
| Missed-event replay | trades or crossings found by an independent daily refetch of 5 sampled pools that our tape missed | ≤ 1% |
| Silent stall count | stalls found by the replay or by hand that the supervisor did not flag | 0 |
| Credit runway | days to exhaustion per paid provider at the 7-day burn rate | ≥ 30 days |
A day passes only if every row passes; the gate is 14 consecutive passing days, reset on failure. Today, by my reading of tonight's data, the BSC tape fails observed-time share and the Solana tape fails all six, so the gate clock has not started.
One builder (Bolo), Vesper for specs and checks. Day estimates are honest working days for one builder including tests and a live soak, not calendar days. "Zero design input" means it does not wait for T5/T8.
| # | Item | Depends on | Days | Starts |
|---|---|---|---|---|
| 1 | Stop the bleeding: retire the two crash-looping units, kill the five hung wrangler deploys, stop the BSC tape retrying out-of-window pools (mark archive_unavailable, stop), get a free NodeReal key for BSC logs |
none | 0.5 | tomorrow, zero design input |
| 2 | Supervisor and coverage rows: progress heartbeats, sd_notify watchdog, stall rules, coverage table, /status page; retrofit to the existing tapes, Fomo watcher and price poll |
none | 2 | tomorrow, zero design input |
| 3 | Event standard as a library (write path enforcing providerAt, availableAt, sha256, quality) and the raw sidecar; price collector moved onto it with unchanged and phantom flags (audit fixes 1–3) |
none; Bolo's staged items 1–3 are reusable | 2 | tomorrow, zero design input |
| 4 | EVM tape v2: one address-array getLogs per window per chain, coverage rows, BSC + Base, Robinhood on decision D4 | 2, 3 | 2 | day 2–3 |
| 5 | Solana tape v2: filtered subscription, swap parser for the main AMMs (Raydium, Pump AMM, Meteora, Orca), per-tx fetch only for gap repair with a credit budget | 2, 3; provider decision D2 | 3–4 | day 3 |
| 6 | Storage layout + query layer: Parquet day close, DuckDB caverio.query, one candle schema from tape, migration of the tapes and events.sqlite (audit fix 9) |
3, 4 | 2 | day 4 |
| 7 | Attention collectors: Telegram client session for callers and chats, message store, Fomo moved onto the event library | 3; T4 source list | 2 | day 3, needs T4 only |
| 8 | Extraction and stance pipeline: prefilter, batched extraction, stance with versioned prompts, cost ledger and caps | 7; T4 prompt spec | 2–3 | day 5 |
| 9 | Derived fields v1: per-minute flow, buyers, holders, band membership and peer comparison, attention aggregates | 6, 8; T5 field list | 3 | waits for T5 |
| 10 | Situation store and lights | 9; T5 rules | 2 | waits for T5 |
| 11 | Macro collector + brief series | 3; T3 metric list | 2 | waits for T3 |
| 12 | Read models + API (documents per view, ETags, auth, per-user rate limits) | 9, 10, 11; T8 wireframes | 3 | waits for T8 |
| 13 | Retire the experimentation stack: server, price_paths (bench copy only), holdings, social-sweep/reader, proof services; archive frozen stores | 12 live for 3 days | 1 | after 12 |
| 14 | Gate instrumentation: daily replay against an independent refetch, the six metrics in §6.4 on /status |
2, 6 | 1.5 | day 5 |
Total: about 28–30 builder days for the whole lane; 8–9 days until items 1–6 give a supervised, gap-accounted tape and price store on the new standard, which is the foundation everything else sits on. This is consistent with the ops critic's 12–20 days for a much smaller scope, and it is not "a few days". What can honestly be done in a few days is items 1–3, and they should start tomorrow because they do not depend on any design choice.
du -h sizes, not two exact byte snapshots; a second byte snapshot (scripts/T2-disk.sh) tomorrow gives the exact rate.Status: DONE