The chosen next increment for solv1-alpha after the data-source pivot. Because Cloudflare blocks non-browser clients, ingestion becomes a browser-harvest-to-JSON step (Claude drives a same-origin fetch() in claude-in-chrome) feeding a Rust file-ingester that lands rows in solv1_alpha_trades; the analysis stack A8–A16 is reused unchanged. Full scope chosen 2026-07-04 — and now BUILT (G2 858b5b1, G3 ac2b6c1, G4 d4e6998; 56 tests; migration 0003_solv1_alpha_klines). The first run on this path produced the project’s first verdict: NO-GO on naive copy-trading.
Status: BUILT (2026-07-04)
All tasks landed. Two things changed vs the plan below: (1) decay is now priced from GMGN 1-second klines, not per-fill streams — see Decay Pivot — per-fill streams → 1s klines; (2) the harvest bridge is a blob download to
~/Downloads, not a localhost POST — see Harvest Bridge (as built). The two-phase structure and the reuse of A8–A16 held exactly as designed.
For Agents
This increment replaces RPC-indexing tasks A4–A7 with a browser-harvest + file-ingest path — see solv1-alpha-data-source-pivot-2026-07-04 for why (non-archival Chainstack node + bot-polluted leaderboard). It is deliberately semi-manual and two-phase: an agent harvests one batch of JSON, a Rust stage consumes it, then — once the cohort is known — the agent harvests a second batch (the cohort tokens’ full fill streams) for the decay/mirror stage.
Scope = Full (chosen over a thinner slice): all five tasks G1–G5 below. The bot/dead filter that Root-Cause-2 of the pivot demands lives in G1 (activity-rate + GMGN smartmoney flag), so the leaderboard pollution is handled at harvest time, before scoring.
Why browser-harvest (the constraint)
Rust cannot call GMGN directly: headless curl/reqwest get a Cloudflare 403, and only a real browser context with a live cf_clearance cookie passes. So the network fetch runs inside claude-in-chrome (same-origin fetch() against /api/v1/wallet_activity/sol?wallet=<addr>&cursor=<data.next>, ~20 events/page, cursor-paginated), writes JSON to disk, and the Rust side reads files, never sockets. Full endpoint/schema detail: solv1-alpha-data-source-pivot-2026-07-04.
Two-Phase Run
The run is split because the second harvest can’t be enumerated until the first analysis picks the cohort — you only know which tokens’ fill streams to pull after scoring/filtering/cohort-selection has named the wallets and their tokens.
graph TD subgraph P1["Phase 1 — wallets"] H1["harvest wallet_activity<br/>(browser · G1)"] --> I1["ingest → solv1_alpha_trades<br/>(G3)"] I1 --> SC["score → filters → cohort<br/>(A8–A12, reused)"] end SC -->|"cohort wallets + their tokens"| P2 subgraph P2["Phase 2 — cohort-token fills"] H2["harvest full fill streams<br/>for cohort tokens<br/>(browser · G4)"] --> I2["ingest fills (G4)"] I2 --> MV["mirror (real decay) → benchmarks → verdict<br/>(A13–A16, reused)"] end style SC fill:#2d2d2d,stroke:#e9c46a,color:#fff style MV fill:#2d2d2d,stroke:#e9c46a,color:#fff style H1 fill:#264653,stroke:#2a9d8f,color:#fff style H2 fill:#264653,stroke:#2a9d8f,color:#fff
Phase 2’s per-token fill streams are what let the paper-mirror model real entry/exit decay from the token’s actual subsequent on-chain fills (rather than a fixed haircut) — the same decay contract the built harness already implements.
Tasks (G1–G5)
| Task | Deliverable | Commit | Notes |
|---|---|---|---|
| G1 | Harvester — drive same-origin fetch() over wallet_activity, cursor-paginate, write JSON per wallet | — | Includes the bot/dead pre-filter (activity-rate + GMGN smartmoney flag) that Root-Cause-2 of the pivot requires — pollution is dropped at harvest, before scoring. In the first run it auto-rejected 84 of 121 harvested wallets as bots |
| G2 | JSON→Trade mapper — map the GMGN event schema to the harness Trade type | 858b5b1 | synthetic slot ← timestamp; buy/sell sign; quote_amount as SOL notional; priority_fee + tip_fee; dex venue |
| G3 | Wallet ingest + Phase A — file-ingester loads harvested wallet-activity JSON into solv1_alpha_trades, runs score/filters/cohort | ac2b6c1 | Phase-1 sink; feeds score/filters/cohort |
| G4 | Kline-decay + Phase B — harvest 1-second klines per cohort token, ingest into solv1_alpha_klines, run mirror/bench/verdict | d4e6998 | Pivoted from per-fill streams to 1s klines (per-fill harvest of 391 tokens was infeasible; klines give ~100% coverage cheaply). Adds migration 0003_solv1_alpha_klines. See |
| G5 | run_all rewire — wire the two-phase flow into orchestration so score→verdict runs on ingested data | (folded into G3/G4) | Sequences Phase 1 → cohort → Phase 2 → verdict |
Build totals: commits 858b5b1 · ac2b6c1 · d4e6998, 56 tests green, migration 0003_solv1_alpha_klines added to the solv1_alpha_* schema.
Decay Pivot — per-fill streams → 1s klines
The plan’s Phase 2 was to harvest each cohort token’s full subsequent-fill stream so the paper-mirror could model follower entry/exit decay from actual on-chain fills. That turned out to be infeasible at scale: the first run touched 391 tokens, and a full per-fill harvest of all of them blows past GMGN’s session rate limit (see below).
The fix: price decay from GMGN 1-second klines instead of raw fills.
Kline decay endpoint
GET /api/v1/token_kline/sol/<token>?resolution=1sOne request per token returns a dense 1-second OHLC series — enough temporal resolution to model a follower’s few-hundred-ms-late fill against the real price path, at ~100% token coverage for a single cheap request per token instead of paginating an unbounded fill stream. Landed in
solv1_alpha_klines(migration0003). In the first run this priced 231 positions with real decay.
This is a strictly cheaper substitute for the same decay contract the built harness expects — the mirror still models real price movement between the leader’s fill and the follower’s, it just reads it off a kline bar rather than off the next raw fills.
Harvest Bridge (as built)
Getting harvested JSON from the browser to disk was the fiddly part. The as-built bridge:
- Browser same-origin
fetch()→ blob → download to~/Downloads. The page fetches GMGN in-context (carryingcf_clearance), wraps the response in a Blob, and triggers a browser download. The Rust ingester then reads the files out of~/Downloads. - A localhost sink does not work — CSP blocks it. Posting the harvested JSON to a local
http://localhostcollector is Content-Security-Policy-blocked from the GMGN origin, so the blob-download path is the only egress that clears both Cloudflare (inbound) and CSP (outbound). - One-time permission: the browser download path needs the site’s “automatic downloads” permission granted once, after which the loop runs unattended.
- Fire-and-forget async loop: a page-side async loop harvests unattended (no per-request babysitting) — kick it off, let it drain.
- Rate ceiling: GMGN throttles at ~1.3k fetches/session. This is the hard cap that made the 391-token full-fill harvest infeasible and forced the 1-request-per-token kline pivot above.
The rate limit is the design constraint
GMGN rate-limits at ~1,300 fetches per session. Any Phase-2 design that scales per fill (unbounded pages per token × hundreds of tokens) will exhaust the session; the 1-request-per-token kline design fits inside the budget. Plan future harvests around this ceiling, not around ideal data richness.
Reused vs Replaced
- Replaced (A4–A7):
rpc.rs,index.rs,swap.rs, universe indexing → G1–G4 (harvest + file-ingest). - Reused unchanged (A8–A16): score → filters → cohort → decay → mirror → benchmarks → verdict. These read
solv1_alpha_tradesand don’t care how it was filled — which is exactly why the pivot is cheap.
Semi-manual by design, not by omission
There is a human/agent-in-the-loop harvest between the Rust stages (and again between Phase 1 and Phase 2). This is intentional — it is the price of clearing Cloudflare with a real browser session. It is not a candidate for “just call the API from Rust”; that path is the 403 the pivot ruled out.
Related
- solv1-alpha-first-verdict-no-go-2026-07-04 — the first verdict this built path produced: NO-GO on naive copy-trading (position-holds edge is the refinement)
- solv1-alpha-data-source-pivot-2026-07-04 — why this exists (non-archival node + polluted leaderboard) + the endpoint/schema (read this first)
- solv1-alpha-validation-backtest-2026-07-04 — the built harness whose A8–A16 stages this feeds
- slv1 — project overview / strategy pipeline
- supabase-from-gcp-connection-gotchas — the
solv1_alpha_*storage connection recipe - LOG · TOPICS