From 16809a183e0b268e55b7afd3cd8674a8de5d99f6 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:41:12 +0200 Subject: [PATCH 01/18] feat: paginate getSignaturesForAddress for complete tx coverage --- harnesses/solana-exec/cmd/collector/main.go | 28 ++++++++++++------- .../solana-exec/internal/helius/client.go | 13 +++++---- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index 1f4b2338..eb494555 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -45,20 +45,28 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun } const sigLimit = 100 - // Fetch signatures newer than last seen. Results are newest-first. - sigs, err := h.GetSignaturesForAddress(ctx, feeAccount, sigLimit, cursor.LastSig) - if err != nil { - return fmt.Errorf("get sigs: %w", err) + // Paginate through ALL signatures newer than cursor (newest-first per page). + // Each page uses `before=oldestSigInPreviousPage` to walk backwards until + // we exhaust the window. This guarantees complete coverage regardless of volume. + var sigs []helius.SigEntry + before := "" + for { + batch, err := h.GetSignaturesForAddress(ctx, feeAccount, sigLimit, cursor.LastSig, before) + if err != nil { + return fmt.Errorf("get sigs (before=%s): %w", before, err) + } + sigs = append(sigs, batch...) + if len(batch) < sigLimit { + break // last page + } + before = batch[len(batch)-1].Signature + time.Sleep(300 * time.Millisecond) // respect Helius free-tier rate limit between pages } if len(sigs) == 0 { return nil } - // On incremental polls (cursor set), hitting the cap means we're dropping older - // txs from this window — tx counts will be understated, though fee stats remain - // a representative sample of the most-recent transactions. - // The initial bootstrap (no cursor) always hits the cap; that's expected. - if len(sigs) == sigLimit && cursor.LastSig != "" { - log.Printf("collector: %s: WARNING hit sig limit (%d) — older txs in this poll window dropped; reduce POLL_INTERVAL or increase limit", plt, sigLimit) + if len(sigs) > sigLimit { + log.Printf("collector: %s: paginated %d raw sigs (%d pages)", plt, len(sigs), (len(sigs)+sigLimit-1)/sigLimit) } // Reverse to process oldest-first so cursor is always the true watermark. diff --git a/harnesses/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go index b35fd148..45bc6c17 100644 --- a/harnesses/solana-exec/internal/helius/client.go +++ b/harnesses/solana-exec/internal/helius/client.go @@ -34,11 +34,11 @@ type SigEntry struct { Err any `json:"err"` // nil = success } -// GetSignaturesForAddress fetches up to `limit` finalized signatures -// for `address`. Pass `until=""` to get the most recent; pass a sig -// to get only transactions newer than that sig (exclusive). -// Results are returned newest-first. -func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, limit int, until string) ([]SigEntry, error) { +// GetSignaturesForAddress fetches up to `limit` finalized signatures for +// `address`, newest-first. `until` is the exclusive upper bound (cursor); +// `before` is the exclusive lower bound used for pagination (pass "" for +// the first page). +func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, limit int, until, before string) ([]SigEntry, error) { params := map[string]any{ "limit": limit, "commitment": "finalized", @@ -46,6 +46,9 @@ func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, li if until != "" { params["until"] = until } + if before != "" { + params["before"] = before + } body, _ := json.Marshal(map[string]any{ "jsonrpc": "2.0", From 8a035124dd9823a5dfd00c921470ae4c598c7966 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:43:28 +0200 Subject: [PATCH 02/18] fix(codex-ws): in-process chromedp scraper for fresh JWE every 5min --- harnesses/aggregator-head-lag/Dockerfile | 5 +- .../cmd/scrape-cookie/main.go | 171 +++++++++--------- .../cmd/script/codex_scraper.go | 141 +++++++++++++++ .../cmd/script/defined_auth.go | 10 +- .../aggregator-head-lag/cmd/script/main.go | 5 + 5 files changed, 244 insertions(+), 88 deletions(-) create mode 100644 harnesses/aggregator-head-lag/cmd/script/codex_scraper.go diff --git a/harnesses/aggregator-head-lag/Dockerfile b/harnesses/aggregator-head-lag/Dockerfile index 74ca8f0f..777b00d3 100644 --- a/harnesses/aggregator-head-lag/Dockerfile +++ b/harnesses/aggregator-head-lag/Dockerfile @@ -21,9 +21,12 @@ FROM debian:bookworm-slim WORKDIR /app -# Install runtime dependencies +# Install runtime dependencies + Chromium for in-process JWE scraping RUN apt-get update && apt-get install -y \ ca-certificates \ + chromium \ + chromium-sandbox \ + fonts-liberation \ && rm -rf /var/lib/apt/lists/* # Copy binary from builder diff --git a/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go b/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go index 686fa5d6..f64c3df5 100644 --- a/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go +++ b/harnesses/aggregator-head-lag/cmd/scrape-cookie/main.go @@ -1,12 +1,9 @@ package main import ( - "bytes" "context" "encoding/json" "fmt" - "io" - "net/http" "net/url" "os" "time" @@ -28,16 +25,66 @@ func main() { defer cancel() ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() - ctx, cancel = context.WithTimeout(ctx, 45*time.Second) + ctx, cancel = context.WithTimeout(ctx, 60*time.Second) defer cancel() cookies := map[string]string{} + // Monkey-patch WebSocket to capture connection_init sent to graph.codex.io + // and the server's first response (ack or error/close). + captureScript := ` +window.__wsCaptures = []; +window.__wsResponses = []; +const _WS = window.WebSocket; +window.WebSocket = function(url, protocols) { + const ws = new _WS(url, protocols); + if (url && url.includes('graph.codex.io')) { + const _send = ws.send.bind(ws); + ws.send = function(data) { + try { + const parsed = JSON.parse(data); + if (parsed.type === 'connection_init') { + window.__wsCaptures.push({url: url, payload: parsed.payload, raw: data}); + console.log('WS_CAPTURE:' + JSON.stringify({url, payload: parsed.payload})); + } + } catch(e) {} + return _send(data); + }; + ws.addEventListener('message', function(evt) { + try { + const parsed = JSON.parse(evt.data); + window.__wsResponses.push({url: url, type: parsed.type, raw: evt.data.slice(0, 200)}); + console.log('WS_RESPONSE:' + JSON.stringify({url, type: parsed.type})); + } catch(e) {} + }); + ws.addEventListener('close', function(evt) { + window.__wsResponses.push({url: url, closeCode: evt.code, reason: evt.reason}); + console.log('WS_CLOSE:' + JSON.stringify({url, code: evt.code, reason: evt.reason})); + }); + } + return ws; +}; +window.WebSocket.prototype = _WS.prototype; +window.WebSocket.CONNECTING = _WS.CONNECTING; +window.WebSocket.OPEN = _WS.OPEN; +window.WebSocket.CLOSING = _WS.CLOSING; +window.WebSocket.CLOSED = _WS.CLOSED; +` + err := chromedp.Run(ctx, + // Inject monkey-patch before page loads + chromedp.ActionFunc(func(ctx context.Context) error { + return chromedp.Evaluate(`void 0`, nil).Do(ctx) // warm up + }), + chromedp.Navigate("about:blank"), + chromedp.Evaluate(captureScript, nil), chromedp.Navigate("https://www.defined.fi/"), chromedp.WaitVisible(`body`, chromedp.ByQuery), - chromedp.Sleep(8*time.Second), + // Re-inject on the loaded page (navigate clears scripts) + chromedp.Evaluate(captureScript, nil), + chromedp.Sleep(20*time.Second), chromedp.ActionFunc(func(ctx context.Context) error { + // Get cookies cookieParams, err := network.GetCookies().Do(ctx) if err != nil { return fmt.Errorf("failed to get cookies: %w", err) @@ -48,41 +95,49 @@ func main() { } return nil }), + // Capture what the browser sent in connection_init + chromedp.ActionFunc(func(ctx context.Context) error { + var captures []map[string]interface{} + err := chromedp.Evaluate(`window.__wsCaptures || []`, &captures).Do(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "capture eval error: %v\n", err) + return nil + } + if len(captures) == 0 { + fmt.Fprintf(os.Stderr, "\n[WS CAPTURE] No connection_init captured for graph.codex.io\n") + // Try to get captures via console approach + var raw string + chromedp.Evaluate(`JSON.stringify(window.__wsCaptures || [])`, &raw).Do(ctx) + fmt.Fprintf(os.Stderr, "[WS CAPTURE] raw: %s\n", raw) + } + for i, c := range captures { + fmt.Fprintf(os.Stderr, "\n[WS CAPTURE #%d] url=%v\n", i, c["url"]) + payloadBytes, _ := json.MarshalIndent(c["payload"], "", " ") + fmt.Fprintf(os.Stderr, "[WS CAPTURE #%d] connection_init payload:\n%s\n", i, string(payloadBytes)) + fmt.Printf("WS_INIT_PAYLOAD=%s\n", string(payloadBytes)) + } + // Capture server responses (ack / close) + var responses []map[string]interface{} + chromedp.Evaluate(`window.__wsResponses || []`, &responses).Do(ctx) + if len(responses) == 0 { + fmt.Fprintf(os.Stderr, "\n[WS RESPONSE] No server responses captured yet\n") + } + for i, r := range responses { + respBytes, _ := json.MarshalIndent(r, "", " ") + fmt.Fprintf(os.Stderr, "\n[WS RESPONSE #%d]:\n%s\n", i, string(respBytes)) + } + return nil + }), ) if err != nil { fmt.Fprintf(os.Stderr, "chrome error: %v\n", err) os.Exit(1) } - // Try codex_token first (it's a pre-minted token, might be usable directly) - // It's stored as URL-encoded JSON: {"token":""} - if raw, ok := cookies["codex_token"]; ok { - decoded, err := url.QueryUnescape(raw) - if err == nil { - var obj struct { - Token string `json:"token"` - } - if json.Unmarshal([]byte(decoded), &obj) == nil && obj.Token != "" { - fmt.Fprintf(os.Stderr, "\ncodex_token JWT (len=%d): %s...\n", len(obj.Token), obj.Token[:min(60, len(obj.Token))]) - // Test if we can use this token directly for a Codex query - testCodexToken(obj.Token) - } - } - } - - // Also test defined-attestation-token for JWT minting - if attToken, ok := cookies["defined-attestation-token"]; ok { - fmt.Fprintf(os.Stderr, "\nTesting defined-attestation-token for JWT mint...\n") - testJWTMint(attToken, "defined-attestation-token") - } - - // Output fresh defined-attestation-token + // Output cookies if attToken, ok := cookies["defined-attestation-token"]; ok { fmt.Printf("DEFINED_SESSION_COOKIE=%s\n", attToken) - fmt.Printf("COOKIE_NAME=defined-attestation-token\n") } - - // Also output full codex_token JSON decoded token if raw, ok := cookies["codex_token"]; ok { decoded, _ := url.QueryUnescape(raw) var obj struct { @@ -93,57 +148,3 @@ func main() { } } } - -func testJWTMint(cookieValue, cookieName string) { - reqBody := map[string]interface{}{ - "operationName": "CreateApiToken", - "query": "mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }", - "variables": map[string]interface{}{}, - } - bodyBytes, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", "https://www.defined.fi/api", bytes.NewBuffer(bodyBytes)) - req.Header.Set("Accept", "application/json") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Origin", "https://www.defined.fi") - req.Header.Set("Referer", "https://www.defined.fi/") - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") - req.AddCookie(&http.Cookie{Name: cookieName, Value: cookieValue}) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - fmt.Fprintf(os.Stderr, "mint request failed: %v\n", err) - return - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - fmt.Fprintf(os.Stderr, "mint status: %d, body: %s\n", resp.StatusCode, string(body[:min(200, len(body))])) -} - -func testCodexToken(token string) { - // Try using the token directly as a Bearer for Codex GraphQL - reqBody := map[string]interface{}{ - "query": "{ getNetworkStats(networkId: 1) { addressCount } }", - } - bodyBytes, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", "https://graph.codex.io/graphql", bytes.NewBuffer(bodyBytes)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - fmt.Fprintf(os.Stderr, "codex token test failed: %v\n", err) - return - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - fmt.Fprintf(os.Stderr, "codex test status: %d, body: %s\n", resp.StatusCode, string(body[:min(200, len(body))])) -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go new file mode 100644 index 00000000..6ec7cac7 --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go @@ -0,0 +1,141 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "strings" + "sync" + "time" + + "github.com/chromedp/cdproto/network" + "github.com/chromedp/chromedp" +) + +// in-process JWE refresher: scrapes defined.fi every 5 min via headless Chrome. +// Result is stored here so GetDefinedJWTToken can use it without hitting the Paris box. +var inProcessJWE struct { + sync.RWMutex + token string + mintedAt time.Time +} + +func getInProcessJWE() (string, time.Time) { + inProcessJWE.RLock() + defer inProcessJWE.RUnlock() + return inProcessJWE.token, inProcessJWE.mintedAt +} + +func setInProcessJWE(token string) { + inProcessJWE.Lock() + defer inProcessJWE.Unlock() + inProcessJWE.token = token + inProcessJWE.mintedAt = time.Now() +} + +// scrapeCodexToken runs headless Chrome, navigates to defined.fi, and extracts the codex_token JWE. +func scrapeCodexToken() (string, error) { + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.UserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"), + ) + + // Allow overriding Chrome binary path via env (useful for Alpine/Debian containers). + if chromePath := os.Getenv("CHROME_PATH"); chromePath == "" { + // Try common locations + for _, p := range []string{"/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome"} { + if _, err := os.Stat(p); err == nil { + opts = append(opts, chromedp.ExecPath(p)) + break + } + } + } else { + opts = append(opts, chromedp.ExecPath(chromePath)) + } + + allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer allocCancel() + + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + + ctx, cancel = context.WithTimeout(ctx, 90*time.Second) + defer cancel() + + var cookies []*network.Cookie + err := chromedp.Run(ctx, + chromedp.Navigate("https://www.defined.fi/"), + chromedp.WaitVisible(`body`, chromedp.ByQuery), + chromedp.Sleep(20*time.Second), + chromedp.ActionFunc(func(ctx context.Context) error { + var err error + cookies, err = network.GetCookies().Do(ctx) + return err + }), + ) + if err != nil { + return "", fmt.Errorf("chromedp run failed: %w", err) + } + + for _, c := range cookies { + if c.Name == "codex_token" { + decoded, err := url.QueryUnescape(c.Value) + if err != nil { + return "", fmt.Errorf("url unescape failed: %w", err) + } + var obj struct { + Token string `json:"token"` + } + if err := json.Unmarshal([]byte(decoded), &obj); err == nil && obj.Token != "" { + return obj.Token, nil + } + // Fallback: if the cookie value is the JWE directly + if strings.HasPrefix(decoded, "eyJ") { + return decoded, nil + } + } + } + return "", fmt.Errorf("codex_token cookie not found after page load") +} + +// startInProcessScraper launches a background goroutine that refreshes the JWE every 5 min. +// Call once from main. Safe to call even if Chrome is not installed (logs error, no crash). +func startInProcessScraper(stopChan <-chan struct{}) { + go func() { + // Initial delay: let the container fully start before launching Chrome. + select { + case <-stopChan: + return + case <-time.After(10 * time.Second): + } + + refreshInterval := 5 * time.Minute + + for { + fmt.Println("[CODEX-SCRAPER] Scraping defined.fi for fresh JWE...") + tok, err := scrapeCodexToken() + if err != nil { + fmt.Printf("[CODEX-SCRAPER] Scrape failed: %v — retrying in 60s\n", err) + select { + case <-stopChan: + return + case <-time.After(60 * time.Second): + continue + } + } + setInProcessJWE(tok) + fmt.Printf("[CODEX-SCRAPER] Fresh JWE stored (len=%d), next refresh in %v\n", len(tok), refreshInterval) + + select { + case <-stopChan: + return + case <-time.After(refreshInterval): + } + } + }() +} diff --git a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go index 62728c56..987a1f13 100644 --- a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go +++ b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go @@ -130,17 +130,23 @@ func tryTokenService(baseURL string) (string, error) { } // GetDefinedJWTToken returns a cached JWT token or generates a new one if expired. -// Priority: CODEX_JWT env var > direct /api/codex/token (same IP as WS) > sidecar > inline mint. +// Priority: CODEX_JWT env var > in-process scraper (fresh, <5min) > direct /api/codex/token > sidecar > inline mint. func GetDefinedJWTToken(sessionCookie string) (string, error) { if jwt := os.Getenv("CODEX_JWT"); jwt != "" { return jwt, nil } + // In-process scraper: headless Chrome running inside this container, freshest possible. + // JWE expires in ~10 min; scraper refreshes every 5 min so token is always <5 min old. + if tok, mintedAt := getInProcessJWE(); tok != "" && time.Since(mintedAt) < 8*time.Minute { + fmt.Printf("[DEFINED-AUTH] Got token from in-process scraper (age=%v, len=%d)\n", time.Since(mintedAt).Round(time.Second), len(tok)) + return tok, nil + } // Direct mint: JWE minted from this container's IP = same IP used for WS = no 4403. if tok, err := tryDirectCodexToken(sessionCookie); err == nil && tok != "" { fmt.Printf("[DEFINED-AUTH] Got token via direct /api/codex/token (len=%d)\n", len(tok)) return tok, nil } - // Sidecar fallback (Paris box chromedp, auto-refreshes every 25 min) + // Sidecar fallback (Paris box chromedp, auto-refreshes every 25 min — may be stale) if svcURL := os.Getenv("DEFINED_TOKEN_SERVICE_URL"); svcURL != "" { if tok, err := tryTokenService(svcURL); err == nil && tok != "" { fmt.Printf("[DEFINED-AUTH] Got token from sidecar (len=%d)\n", len(tok)) diff --git a/harnesses/aggregator-head-lag/cmd/script/main.go b/harnesses/aggregator-head-lag/cmd/script/main.go index 84444379..a545d832 100644 --- a/harnesses/aggregator-head-lag/cmd/script/main.go +++ b/harnesses/aggregator-head-lag/cmd/script/main.go @@ -38,6 +38,11 @@ func main() { var wg sync.WaitGroup stopChan := make(chan struct{}) + // In-process JWE scraper: headless Chrome inside this container scrapes defined.fi + // every 5 min to keep the Codex token fresh. JWE expires in ~10 min, so 5 min gives + // a comfortable margin. Runs only if Chrome is installed (no-op otherwise). + startInProcessScraper(stopChan) + wg.Add(1) go func() { defer wg.Done() From e29db6f76afe4456cb62e6b56d2bd73c73874199 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:46:19 +0200 Subject: [PATCH 03/18] fix: remove dead providers from xdc/etc-rpc, allow optional p90/p99 in load.ts --- benchmarks/ethereum-classic-rpc.yml | 60 +++------------------ benchmarks/xdc-rpc.yml | 83 +++-------------------------- src/lib/materialize/load.ts | 5 ++ 3 files changed, 19 insertions(+), 129 deletions(-) diff --git a/benchmarks/ethereum-classic-rpc.yml b/benchmarks/ethereum-classic-rpc.yml index 6add2964..c7122e78 100644 --- a/benchmarks/ethereum-classic-rpc.yml +++ b/benchmarks/ethereum-classic-rpc.yml @@ -4,7 +4,7 @@ slug: ethereum-classic-rpc number: "194" title: Fastest free Ethereum Classic RPC, live no-key endpoint latency seo_title: "Fastest free Ethereum Classic RPC 2026" -seo_description: "{{best_name}} leads free Ethereum Classic RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 3 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free Ethereum Classic RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 1 no-key provider measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public Ethereum Classic EVM endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - Ethereum Classic (chain 61) is the original Ethereum chain post-2016 DAO fork, continuing as a Proof-of-Work EVM network with ETC as the gas token. The chain upholds code-is-law immutability and has a dedicated long-term holder community. Three keyless endpoints are probed continuously: dRPC (etc.drpc.org), ETCDesktop (etc.etcdesktop.com), and ETCMC (rpc.etcmc.net). + Ethereum Classic (chain 61) is the original Ethereum chain post-2016 DAO fork, continuing as a Proof-of-Work EVM network with ETC as the gas token. The chain upholds code-is-law immutability and has a dedicated long-term holder community. One keyless endpoint is probed continuously: dRPC (etc.drpc.org). ETCDesktop and ETCMC are no longer responding to probes and have been removed. abstract: | Per-chain member of the RPC latency cluster, extended to Ethereum Classic. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public Ethereum Classic endpoint - that sustains continuous probing, 3 providers at launch, + that sustains continuous probing, 1 provider at launch, every 60 seconds, from us-east, eu-west and Singapore. The cross-chain view lives on the parent `rpc-capabilities` benchmark; this page is the Ethereum Classic-scoped answer with per-region breakdowns as a first-class dimension. @@ -31,16 +31,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err`, `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"ethereum-classic\"`. Provider coverage at launch: 3 no-key endpoints." + - "Chain scope: every query on this page is pinned to `chain=\"ethereum-classic\"`. Provider coverage: 1 no-key endpoint (dRPC). ETCDesktop and ETCMC stopped responding to probes and have been removed." findings: - - "{{best_name}} currently leads free Ethereum Classic RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 3 measured providers." + - "{{best_name}} currently leads free Ethereum Classic RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free Ethereum Classic RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 3 no-key providers probed every 60 seconds from us-east, eu-west and Singapore." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 1 no-key provider probed every 60 seconds from us-east, eu-west and Singapore." - q: "Which Ethereum Classic RPCs work without an API key?" - a: "3 endpoints sustain continuous keyless probing: dRPC, ETCDesktop, and ETCMC. Every endpoint was live-verified before inclusion." + a: "1 endpoint sustains continuous keyless probing: dRPC (etc.drpc.org). ETCDesktop and ETCMC stopped responding to probes and have been removed from the bench." - q: "Does the fastest Ethereum Classic RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin." - q: "How is Ethereum Classic RPC latency measured here?" @@ -84,49 +84,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="drpc", chain="ethereum-classic", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="drpc", chain="ethereum-classic", region="sgp"}[1h]) - - - slug: etcdesktop - name: ETCDesktop - tag: ETCDesktop community public RPC for Ethereum Classic - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to ETCDesktop's no-key Ethereum Classic endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="etcdesktop", chain="ethereum-classic"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="etcdesktop", chain="ethereum-classic"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="etcdesktop", chain="ethereum-classic"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="etcdesktop", chain="ethereum-classic"}) / sum(ocb:rpc_call:rate_24h{provider="etcdesktop", chain="ethereum-classic"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="etcdesktop", chain="ethereum-classic"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcdesktop", chain="ethereum-classic", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcdesktop", chain="ethereum-classic", region="sgp"}[1h]) - - - slug: etcmc - name: ETCMC - tag: ETCMC community public RPC for Ethereum Classic - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions to ETCMC's no-key Ethereum Classic endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="etcmc", chain="ethereum-classic"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="etcmc", chain="ethereum-classic"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="etcmc", chain="ethereum-classic"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="etcmc", chain="ethereum-classic"}) / sum(ocb:rpc_call:rate_24h{provider="etcmc", chain="ethereum-classic"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="etcmc", chain="ethereum-classic"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="etcmc", chain="ethereum-classic", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="etcmc", chain="ethereum-classic", region="sgp"}[1h]) diff --git a/benchmarks/xdc-rpc.yml b/benchmarks/xdc-rpc.yml index e4c64a92..493e78f5 100644 --- a/benchmarks/xdc-rpc.yml +++ b/benchmarks/xdc-rpc.yml @@ -4,7 +4,7 @@ slug: xdc-rpc number: "153" title: Fastest free XDC Network RPC, live no-key endpoint latency seo_title: "Fastest free XDC Network RPC 2026" -seo_description: "{{best_name}} leads free XDC Network RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 5 no-key providers measured every 60s from 3 regions." +seo_description: "{{best_name}} leads free XDC Network RPC at {{best_p50}} (eth_getBlockByNumber p50, 24h). 2 no-key providers measured every 60s from 3 regions." subtitle: HTTP round-trip latency for eth_getBlockByNumber against every free, no-key public XDC Network RPC endpoint, audited every 60 seconds from 3 regions. category: RPCs @@ -14,13 +14,13 @@ unit: ms higher_is_better: false seo_intro: | - XDC Network (chain 50) is an EVM-compatible hybrid blockchain targeting enterprise and trade-finance use cases, XDC gas token, delegated proof of stake consensus, designed for ISO 20022-compliant financial applications. The XDC Foundation's rpc.xinfin.network and additional endpoints from Ankr and the community provide keyless access probed every 60 seconds from three regions. + XDC Network (chain 50) is an EVM-compatible hybrid blockchain targeting enterprise and trade-finance use cases, XDC gas token, delegated proof of stake consensus, designed for ISO 20022-compliant financial applications. Two keyless endpoints are probed continuously: Ankr and XDC eRPC (erpc.xinfin.network). XDC Foundation official, XDCrpc, and XDC.org stopped responding to probes and have been removed. abstract: | Per-chain member of the RPC latency cluster, extended to XDC Network. We measure the round-trip latency of a single, identical RPC call (`eth_getBlockByNumber`) against every no-key public XDC Network endpoint - that sustains continuous probing, 5 providers at launch, + that sustains continuous probing, 2 providers, every 60 seconds, from us-east, eu-west and Singapore. The harness classifies every response (ok / http_err / jsonrpc_err / stale / timeout) so the leaderboard rewards sustained availability rather than @@ -34,16 +34,16 @@ methodology: - "Latency: client-side round-trip delta in milliseconds, exposed as both a gauge and a histogram (buckets 50 ms to 10 s), so percentiles are computed via Prometheus `quantile_over_time` over the last 24 hours." - "Call-result classification: `ok` (HTTP 200 + non-empty result), `http_err`, `jsonrpc_err` (HTTP 200 carrying an error body), `stale` (more than 20 blocks behind the cross-provider tip), `timeout`." - "This page is part of the per-chain RPC cluster derived from the cross-chain [rpc-capabilities](https://openchainbench.com/benchmarks/rpc-capabilities) benchmark; the identical harness, cadence and exclusion rules apply on every chain." - - "Chain scope: every query on this page is pinned to `chain=\"xdc\"`. Provider coverage at launch: 5 no-key endpoints (XDC Foundation, Ankr, XDCrpc, XDC eRPC, XDC.org)." + - "Chain scope: every query on this page is pinned to `chain=\"xdc\"`. Provider coverage: 2 no-key endpoints (Ankr, XDC eRPC). XDC Foundation official, XDCrpc and XDC.org stopped responding to probes and have been removed." findings: - - "{{best_name}} currently leads free XDC Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 5 measured providers." + - "{{best_name}} currently leads free XDC Network RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 2 measured providers." faq: - q: "What is the fastest free XDC Network RPC right now?" - a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 5 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." + a: "{{best_name}} currently leads at {{best_p50}} (`eth_getBlockByNumber` p50 over the last 24h), measured against 2 no-key providers probed every 60 seconds from us-east, eu-west and Singapore. The leaderboard re-sorts continuously against fresh Prometheus samples." - q: "Which XDC Network RPCs work without an API key?" - a: "5 endpoints sustain continuous keyless probing at launch: XDC Foundation, Ankr, XDCrpc, XDC eRPC, XDC.org. Every listed endpoint was live-verified before inclusion." + a: "2 endpoints sustain continuous keyless probing: Ankr and XDC eRPC (erpc.xinfin.network). XDC Foundation official, XDCrpc and XDC.org stopped responding to probes and have been removed." - q: "Does the fastest XDC Network RPC change by region?" a: "Often. The region tabs at the top of the page re-scope every number to a single origin; pick the one closest to where your requests originate." - q: "How is XDC Network RPC latency measured here?" @@ -65,29 +65,6 @@ dimensions: - { value: sgp, label: Singapore } providers: - - slug: xdc-official - name: XDC Foundation - tag: XDC Foundation's primary public RPC - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to XDC Foundation's no-key XDC Network endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="xdc-official", chain="xdc"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="xdc-official", chain="xdc"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="xdc-official", chain="xdc"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="xdc-official", chain="xdc"}) / sum(ocb:rpc_call:rate_24h{provider="xdc-official", chain="xdc"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="xdc-official", chain="xdc"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-official", chain="xdc", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-official", chain="xdc", region="sgp"}[1h]) - - slug: ankr name: Ankr tag: Multi-cloud RPC network @@ -111,29 +88,6 @@ providers: p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="ankr", chain="xdc", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="ankr", chain="xdc", region="sgp"}[1h]) - - slug: xdcrpc - name: XDCrpc - tag: Community-operated XDC public RPC - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to XDCrpc's no-key XDC Network endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="xdcrpc", chain="xdc"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="xdcrpc", chain="xdc"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="xdcrpc", chain="xdc"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="xdcrpc", chain="xdc"}) / sum(ocb:rpc_call:rate_24h{provider="xdcrpc", chain="xdc"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="xdcrpc", chain="xdc"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdcrpc", chain="xdc", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdcrpc", chain="xdc", region="sgp"}[1h]) - - slug: xdc-erpc name: XDC eRPC tag: XDC Foundation alternate public RPC gateway @@ -156,26 +110,3 @@ providers: - region: ap-southeast p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-erpc", chain="xdc", region="sgp"}) series: avg_over_time(rpc_latency_milliseconds{provider="xdc-erpc", chain="xdc", region="sgp"}[1h]) - - - slug: xdc-org - name: XDC.org - tag: XDC Foundation third public RPC mirror - formula: "50th percentile over 24h of client-side round-trip latency (ms) for a single `eth_getBlockByNumber` POST sent every 60s from 3 regions (us-east + eu-west + sgp) to XDC Foundation's rpc.xdc.org endpoint." - queries: - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc"}) - p90: avg(ocb:rpc_latency_milliseconds:p90_24h{provider="xdc-org", chain="xdc"}) - p99: avg(ocb:rpc_latency_milliseconds:p99_24h{provider="xdc-org", chain="xdc"}) - mean: avg(ocb:rpc_latency_milliseconds:mean_24h{provider="xdc-org", chain="xdc"}) - success: sum(ocb:rpc_call:ok_rate_24h{provider="xdc-org", chain="xdc"}) / sum(ocb:rpc_call:rate_24h{provider="xdc-org", chain="xdc"}) - sample_size: sum(ocb:rpc_call:increase_24h{provider="xdc-org", chain="xdc"}) - series: avg(avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc"}[1h])) - regions: - - region: us-east - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc", region="us-east"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc", region="us-east"}[1h]) - - region: eu-west - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc", region="eu-west"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc", region="eu-west"}[1h]) - - region: ap-southeast - p50: avg(ocb:rpc_latency_milliseconds:p50_24h{provider="xdc-org", chain="xdc", region="sgp"}) - series: avg_over_time(rpc_latency_milliseconds{provider="xdc-org", chain="xdc", region="sgp"}[1h]) diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index f6271f05..420739a5 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -807,6 +807,11 @@ async function tryLoadLive( // common AWAITING trigger. Logged at warn level only on the // unfiltered "All" view to avoid spamming logs on filtered views // where a missing provider is expected behavior. + // For gauge-only benches (e.g. validator yield) p90/p99 are not + // defined in the spec. Fall back to p50 so the provider isn't skipped. + if (p90 == null && !q.p90) p90 = p50; + if (p99 == null && !q.p99) p99 = p50; + if (p50 == null || p90 == null || p99 == null) { // Unresponsive cohort member: the latency series is gone from // the window (failed probes record no latency, so a fully dead From 6acdc52b8c444093c9e2ae5cd4258248d47fdb8e Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:48:57 +0200 Subject: [PATCH 04/18] feat(solana-exec): accurate tx volume via raw sig pagination + 100-sig enhanced cap --- harnesses/solana-exec/cmd/collector/main.go | 19 ++++++ harnesses/solana-exec/internal/store/store.go | 58 ++++++++++++++----- .../solana-exec/migrations/002_raw_counts.sql | 18 ++++++ 3 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 harnesses/solana-exec/migrations/002_raw_counts.sql diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index eb494555..909c7774 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -85,12 +85,31 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun } } + // Count successful sigs per hour bucket from raw pagination (full volume, no enhanced API cost). + hourBuckets := make(map[time.Time]int64) + for _, s := range reversed { + if s.Err != nil || s.BlockTime == 0 { + continue + } + bucket := time.Unix(s.BlockTime, 0).UTC().Truncate(time.Hour) + hourBuckets[bucket]++ + } + if err := db.UpsertRawCounts(ctx, plt, hourBuckets); err != nil { + return fmt.Errorf("upsert raw counts: %w", err) + } + if len(sigStrs) == 0 { // All sigs in this batch were failed txs; advance cursor and skip. newest := sigs[0] return db.SaveCursor(ctx, plt, newest.Signature, newest.Slot) } + // Cap enhanced API at 100 sigs per poll (Helius free-tier budget: ~432K CUs/month). + // Fee quality metrics are sampled; tx_count comes from raw counts above. + if len(sigStrs) > 100 { + sigStrs = sigStrs[:100] + } + txs, err := h.GetEnhancedTransactions(ctx, sigStrs) if err != nil { return fmt.Errorf("get enhanced txs: %w", err) diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go index 1ed3f826..1564a23d 100644 --- a/harnesses/solana-exec/internal/store/store.go +++ b/harnesses/solana-exec/internal/store/store.go @@ -97,7 +97,36 @@ func (db *DB) SaveCursor(ctx context.Context, platform, lastSig string, slot uin return err } -// Materialize recomputes hourly facts for the given platform and time range. +// UpsertRawCounts stores hourly successful-tx counts from raw sig pagination. +// counts maps bucket_start (hour-truncated UTC) → total successful sigs that hour. +func (db *DB) UpsertRawCounts(ctx context.Context, platform string, counts map[time.Time]int64) error { + if len(counts) == 0 { + return nil + } + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("store: begin: %w", err) + } + defer tx.Rollback(ctx) + for bucket, count := range counts { + _, err := tx.Exec(ctx, ` + INSERT INTO solana_exec_raw_counts (platform, bucket_start, success_count, updated_at) + VALUES ($1, $2, $3, now()) + ON CONFLICT (platform, bucket_start) DO UPDATE SET + success_count = solana_exec_raw_counts.success_count + EXCLUDED.success_count, + updated_at = now()`, + platform, bucket, count, + ) + if err != nil { + return fmt.Errorf("store: upsert raw count %v: %w", bucket, err) + } + } + return tx.Commit(ctx) +} + +// Materialize recomputes hourly facts for the given platform. +// tx_count comes from solana_exec_raw_counts (full volume); fee metrics come +// from the sampled solana_exec_events (representative quality metrics). func (db *DB) Materialize(ctx context.Context, platform string) error { _, err := db.pool.Exec(ctx, ` INSERT INTO solana_exec_facts @@ -105,19 +134,22 @@ func (db *DB) Materialize(ctx context.Context, platform string) error { avg_priority_fee_lamports, p50_cu_price_micro, p95_cu_price_micro, avg_platform_fee_lamports, jito_rate, avg_cu_consumed, computed_at) SELECT - platform, - date_trunc('hour', block_time) AS bucket_start, - COUNT(*) AS tx_count, - AVG(priority_fee_lamports) AS avg_priority_fee_lamports, - COALESCE(percentile_cont(0.5) WITHIN GROUP (ORDER BY cu_price_micro) FILTER (WHERE cu_price_micro > 0), 0) AS p50_cu_price_micro, - COALESCE(percentile_cont(0.95) WITHIN GROUP (ORDER BY cu_price_micro) FILTER (WHERE cu_price_micro > 0), 0) AS p95_cu_price_micro, - AVG(platform_fee_lamports) AS avg_platform_fee_lamports, - AVG(CASE WHEN is_jito_bundle THEN 1.0 ELSE 0.0 END) AS jito_rate, - AVG(cu_consumed) AS avg_cu_consumed, + e.platform, + date_trunc('hour', e.block_time) AS bucket_start, + COALESCE(r.success_count, COUNT(*)) AS tx_count, + AVG(e.priority_fee_lamports) AS avg_priority_fee_lamports, + COALESCE(percentile_cont(0.5) WITHIN GROUP (ORDER BY e.cu_price_micro) FILTER (WHERE e.cu_price_micro > 0), 0) AS p50_cu_price_micro, + COALESCE(percentile_cont(0.95) WITHIN GROUP (ORDER BY e.cu_price_micro) FILTER (WHERE e.cu_price_micro > 0), 0) AS p95_cu_price_micro, + AVG(e.platform_fee_lamports) AS avg_platform_fee_lamports, + AVG(CASE WHEN e.is_jito_bundle THEN 1.0 ELSE 0.0 END) AS jito_rate, + AVG(e.cu_consumed) AS avg_cu_consumed, now() - FROM solana_exec_events - WHERE platform = $1 - GROUP BY platform, date_trunc('hour', block_time) + FROM solana_exec_events e + LEFT JOIN solana_exec_raw_counts r + ON r.platform = e.platform + AND r.bucket_start = date_trunc('hour', e.block_time) + WHERE e.platform = $1 + GROUP BY e.platform, date_trunc('hour', e.block_time), r.success_count ON CONFLICT (platform, bucket_start) DO UPDATE SET tx_count = EXCLUDED.tx_count, avg_priority_fee_lamports = EXCLUDED.avg_priority_fee_lamports, diff --git a/harnesses/solana-exec/migrations/002_raw_counts.sql b/harnesses/solana-exec/migrations/002_raw_counts.sql new file mode 100644 index 00000000..bf8f199a --- /dev/null +++ b/harnesses/solana-exec/migrations/002_raw_counts.sql @@ -0,0 +1,18 @@ +BEGIN; + +-- Hourly raw successful-tx counts derived from getSignaturesForAddress pagination. +-- Counted at zero extra Helius credit cost (no enhanced API needed). +-- Separate from solana_exec_events which only holds fee-quality sampled rows. +CREATE TABLE IF NOT EXISTS solana_exec_raw_counts ( + platform TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + success_count BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (platform, bucket_start) +); + +-- Replace tx_count (INT) in facts with BIGINT to hold real volumes. +ALTER TABLE solana_exec_facts + ALTER COLUMN tx_count TYPE BIGINT; + +COMMIT; From a0f3f449a2441ec59d48f0446e23befd2991244c Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:52:24 +0200 Subject: [PATCH 05/18] remove: delete indexing-freshness bench (no data) --- benchmarks/indexing-freshness.yml | 127 ------------------------------ 1 file changed, 127 deletions(-) delete mode 100644 benchmarks/indexing-freshness.yml diff --git a/benchmarks/indexing-freshness.yml b/benchmarks/indexing-freshness.yml deleted file mode 100644 index 67cc1f92..00000000 --- a/benchmarks/indexing-freshness.yml +++ /dev/null @@ -1,127 +0,0 @@ -# OpenChainBench. Bench № 070 - -slug: indexing-freshness -number: "070" -title: Freshest wallet data API. Zerion, Allium, Mobula -seo_title: "Freshest wallet data API 2026" -seo_description: "How fast do wallet APIs index a new transaction? Zerion, Allium and Mobula measured live on organic Base transfers, second by second." -subtitle: Seconds between an organic transfer confirming on Base and the moment each wallet data API first returns it, measured continuously on real user transactions. -category: Aggregators -status: live -metric: Indexing freshness -unit: sec -higher_is_better: false - -seo_intro: | - Every wallet app, portfolio tracker and exchange faces the same - question. when a user receives funds, how long before the API my - app is built on actually shows it. Providers advertise "real-time" - and "sub-second indexing"; none publish comparable numbers. This - benchmark measures it the only way that cannot be gamed. we pick a - random, organic native transfer from the newest Base block, real - user, different wallet every time, and poll each provider's wallet - transactions API until the tx hash appears. Because the ground - truth is a random real transaction there is no benchmark wallet a - provider could special-case, and every measurement is publicly - re-verifiable from the tx hash. Cohort. Zerion, Allium, - GoldRush (Covalent) and Mobula, all probed with the identical - schedule from the same host. The timing context matters in 2026. - Dune Sim is sunsetting and SimpleHash is gone, so teams are - choosing a replacement wallet data API right now, and freshness is - the spec sheet line that separates them. - -abstract: | - We measure the visibility lag of wallet data APIs. the time between - an organic native transfer confirming on Base (T0 = the instant our - own RPC observes the containing block) and the first poll at which - each provider's wallet transactions endpoint returns that tx hash. - One probe event per 10 minutes; each event uses a fresh random - transaction from a fresh block, so wallets are cold and results - include any lazy, on-demand indexing path a provider runs for - never-before-queried addresses. Detection is parser-free (tx hash - substring in the raw response), the poll schedule is front-loaded - (1s to 120s, identical for every provider), and per-provider - monthly quota guards keep the probe inside every free tier. An - event a provider has not indexed within 120 seconds counts as - missed, which feeds the reliability column, because an API that - never shows the deposit is worse than a slow one. - -methodology: - - "Ground truth: one probe event per 10 minutes. The harness watches new Base blocks through its own RPC and picks one random plain native transfer (value > 0, empty calldata, OP-stack system deposit excluded). T0 is the instant the harness observes the containing block; the same T0 is used for every provider." - - "Anti-gaming by construction: the measured wallet is a random real user's address, different on every event, so no provider can whitelist or pre-warm a known benchmark wallet. Every measurement is re-verifiable by anyone from the public tx hash." - - "Poll schedule: each provider's wallet transactions endpoint is polled at 1, 2, 3, 4, 6, 8, 11, 15, 20, 26, 34, 45, 60, 80, 100 and 120 seconds after T0. Reported lag is the first poll at which the response contains the tx hash, an upper bound with resolution equal to the gap between consecutive polls." - - "Detection is parser-free: the raw response body is scanned for the tx hash substring. Every cohort API returns the hash verbatim, so no provider gains or loses from response schema differences." - - "Cold wallets by design: because each event uses a never-before-queried address, results include any on-demand indexing path a provider runs for new wallets. This mirrors the experience of a user opening an app on a fresh address, and it is disclosed here because warm, continuously-queried wallets may see lower lags." - - "Classification: found (lag recorded), missed (not indexed within 120s), api_error (provider errored on every poll of the event). Percentiles are computed from the histogram of found lags over 24h; the miss rate is published alongside because latency without reliability is a misleading ranking signal." - - "Quota fairness: per-provider monthly call budgets sized to each free tier with headroom, enforced by a guard that pauses probing at 90%. Allium participates in every third event (20k calls/month free tier); all other providers join every event. Sample sizes per provider are published." - - "Scope: Base mainnet, single probe region. Freshness lags are measured in seconds while cross-region network deltas are milliseconds, so multi-region probing would add cost without changing the ranking; this is revisited if two providers converge within one poll-step of each other." - -findings: - - "{{best_name}} currently leads at {{best_p50}} (p50 of found lags, 24h) across organic Base transfers." - - "{{name:zerion}} sits at {{p50:zerion}}. Early runs showed a bimodal pattern, some events indexed in about a second and others surfacing several seconds later, consistent with a caching layer in front of the wallet endpoint." - - "{{name:goldrush}} trails at {{p50:goldrush}} on this probe. Its unified multi-chain schema trades freshness for breadth, a real trade-off teams should weigh explicitly." - - "Miss rates matter more than medians: an API that fails to show a deposit within two minutes breaks the user flow entirely, so read the reliability column before the latency one." - -faq: - - q: "Which wallet data API shows new transactions fastest?" - a: "Per the live leaderboard above: {{best_name}} at {{best_p50}} (p50 over 24h of organic Base transfers). The ranking re-sorts continuously as probe events land every 10 minutes. Check the miss-rate column too, a provider that occasionally never indexes a transfer is worse for a wallet app than one that is a second slower on median." - - q: "How is indexing freshness measured here?" - a: "The harness picks a random organic native transfer from the newest Base block, records T0 when its own RPC observes the block, then polls every provider's wallet transactions endpoint on an identical front-loaded schedule (1s to 120s) until the tx hash appears in the raw response. The lag is the first successful poll. Events a provider has not indexed within 120 seconds count as missed. The full harness is open source and each measurement can be re-verified from the public tx hash." - - q: "Why use random real transactions instead of a controlled test wallet?" - a: "Two reasons. First, integrity: a fixed benchmark wallet could be whitelisted or pre-warmed by a provider; a random real user's transfer, different every event, cannot. Second, realism: cold, never-before-queried addresses exercise any lazy indexing path a provider runs for new wallets, which is exactly what a new user experiences. The trade-off, disclosed in the methodology, is that continuously-queried warm wallets may see lower lags than reported here." - - q: "Does this benchmark cover more chains than Base?" - a: "Not yet. Base was chosen first for its high volume of plain native transfers (dense organic ground truth) and 2-second blocks. The harness is chain-agnostic and additional EVM chains join by adding an RPC endpoint, subject to each provider's free-tier quota budget. Chain coverage itself differs per provider and is part of what teams should evaluate." - - q: "Why does freshness matter when choosing a wallet API in 2026?" - a: "Because the market is consolidating. Dune Sim is sunsetting in August 2026 and SimpleHash shut down in 2025, so many teams are migrating to a new wallet data API right now. Providers advertise real-time indexing but publish no comparable numbers; deposit visibility lag is the difference between a user seeing their funds arrive and a support ticket. This page is the only continuously measured, provider-neutral comparison of that number." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/indexing-freshness - -prometheus: - window: 24h - freshness_metric: indexing_freshness_seconds - -providers: - - slug: mobula - name: Mobula - tag: Wallet + market data API, 50+ chains - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and Mobula's wallet transactions endpoint first returning it, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="mobula"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="mobula"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="mobula"}[24h]))) - success: sum(increase(indexing_probe_total{provider="mobula", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="mobula", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="mobula", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="mobula"}[1h]) - - slug: zerion - name: Zerion - tag: Wallet API behind the Zerion app, 25+ chains - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and Zerion's wallet transactions endpoint first returning it, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="zerion"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="zerion"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="zerion"}[24h]))) - success: sum(increase(indexing_probe_total{provider="zerion", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="zerion", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="zerion", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="zerion"}[1h]) - - slug: goldrush - name: GoldRush - tag: Covalent's multi-chain wallet API, unified schema - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and GoldRush's transactions endpoint first returning it, from the shared histogram estimator." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="goldrush"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="goldrush"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="goldrush"}[24h]))) - success: sum(increase(indexing_probe_total{provider="goldrush", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="goldrush", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="goldrush", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="goldrush"}[1h]) - - slug: allium - name: Allium - tag: Enterprise realtime wallet APIs, 100+ chains - formula: "Median (p50) over 24h of visibility lag in seconds between an organic Base transfer confirming and Allium's wallet transactions endpoint first returning it. Allium joins every third event to respect its free-tier quota." - queries: - p50: histogram_quantile(0.5, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="allium"}[24h]))) - p90: histogram_quantile(0.9, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="allium"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (increase(indexing_freshness_seconds_histogram_bucket{provider="allium"}[24h]))) - success: sum(increase(indexing_probe_total{provider="allium", result="found"}[24h])) / sum(increase(indexing_probe_total{provider="allium", result=~"found|missed"}[24h])) - sample_size: sum(increase(indexing_probe_total{provider="allium", result=~"found|missed"}[24h])) - series: avg_over_time(indexing_freshness_seconds{provider="allium"}[1h]) From 192de2fb58f098ce0ca5e88493d50df2581ffdad Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:53:58 +0200 Subject: [PATCH 06/18] fix(codex): no Chromium in Dockerfile, guard scraper if Chrome absent --- harnesses/aggregator-head-lag/Dockerfile | 5 +--- .../cmd/script/codex_scraper.go | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/harnesses/aggregator-head-lag/Dockerfile b/harnesses/aggregator-head-lag/Dockerfile index 777b00d3..74ca8f0f 100644 --- a/harnesses/aggregator-head-lag/Dockerfile +++ b/harnesses/aggregator-head-lag/Dockerfile @@ -21,12 +21,9 @@ FROM debian:bookworm-slim WORKDIR /app -# Install runtime dependencies + Chromium for in-process JWE scraping +# Install runtime dependencies RUN apt-get update && apt-get install -y \ ca-certificates \ - chromium \ - chromium-sandbox \ - fonts-liberation \ && rm -rf /var/lib/apt/lists/* # Copy binary from builder diff --git a/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go index 6ec7cac7..41740bd4 100644 --- a/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go +++ b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go @@ -103,9 +103,30 @@ func scrapeCodexToken() (string, error) { return "", fmt.Errorf("codex_token cookie not found after page load") } +// chromeAvailable returns true if a Chrome/Chromium binary is found on this host. +func chromeAvailable() bool { + for _, p := range []string{ + os.Getenv("CHROME_PATH"), + "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + } { + if p != "" { + if _, err := os.Stat(p); err == nil { + return true + } + } + } + return false +} + // startInProcessScraper launches a background goroutine that refreshes the JWE every 5 min. -// Call once from main. Safe to call even if Chrome is not installed (logs error, no crash). +// Call once from main. No-ops silently if Chrome is not installed. func startInProcessScraper(stopChan <-chan struct{}) { + if !chromeAvailable() { + fmt.Println("[CODEX-SCRAPER] Chrome not found — in-process scraper disabled (sidecar will be used)") + return + } + go func() { // Initial delay: let the container fully start before launching Chrome. select { From a80a2361addbf43d02180e9723570196f925510b Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:00:03 +0200 Subject: [PATCH 07/18] fix(solana-exec): 100ms RPC sleep + strided sample for fee metrics --- harnesses/solana-exec/cmd/collector/main.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index 909c7774..ace75dbe 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -60,7 +60,7 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun break // last page } before = batch[len(batch)-1].Signature - time.Sleep(300 * time.Millisecond) // respect Helius free-tier rate limit between pages + time.Sleep(100 * time.Millisecond) // Helius free-tier RPC: 10 req/s max } if len(sigs) == 0 { return nil @@ -105,9 +105,14 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun } // Cap enhanced API at 100 sigs per poll (Helius free-tier budget: ~432K CUs/month). - // Fee quality metrics are sampled; tx_count comes from raw counts above. + // Evenly stride across the window so the sample represents the full period, not just the oldest txs. if len(sigStrs) > 100 { - sigStrs = sigStrs[:100] + step := len(sigStrs) / 100 + sampled := make([]string, 0, 100) + for i := 0; i < len(sigStrs) && len(sampled) < 100; i += step { + sampled = append(sampled, sigStrs[i]) + } + sigStrs = sampled } txs, err := h.GetEnhancedTransactions(ctx, sigStrs) From 9978be7e45e2ed71f5f6179f8ce3d733171b14ce Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:18:15 +0200 Subject: [PATCH 08/18] feat(solana-exec): real p50/p95 via cu_samples table + sigLimit 1000 --- harnesses/solana-exec/cmd/api/main.go | 98 +++++++++++-------- harnesses/solana-exec/cmd/collector/main.go | 18 +++- .../solana-exec/cmd/materializer/main.go | 3 + harnesses/solana-exec/internal/store/store.go | 34 +++++++ .../solana-exec/migrations/003_cu_samples.sql | 16 +++ 5 files changed, 125 insertions(+), 44 deletions(-) create mode 100644 harnesses/solana-exec/migrations/003_cu_samples.sql diff --git a/harnesses/solana-exec/cmd/api/main.go b/harnesses/solana-exec/cmd/api/main.go index 3fcf7cac..cccdc7a4 100644 --- a/harnesses/solana-exec/cmd/api/main.go +++ b/harnesses/solana-exec/cmd/api/main.go @@ -62,48 +62,66 @@ func handleExecLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { defer cancel() rows, err := pool.Query(ctx, ` + WITH cu AS ( + SELECT platform, + percentile_cont(0.5) WITHIN GROUP (ORDER BY cu_price_micro) + FILTER (WHERE block_time >= now() - INTERVAL '24 hours') AS h24_p50, + percentile_cont(0.95) WITHIN GROUP (ORDER BY cu_price_micro) + FILTER (WHERE block_time >= now() - INTERVAL '24 hours') AS h24_p95, + percentile_cont(0.5) WITHIN GROUP (ORDER BY cu_price_micro) + FILTER (WHERE block_time >= now() - INTERVAL '7 days') AS d7_p50, + percentile_cont(0.95) WITHIN GROUP (ORDER BY cu_price_micro) + FILTER (WHERE block_time >= now() - INTERVAL '7 days') AS d7_p95, + percentile_cont(0.5) WITHIN GROUP (ORDER BY cu_price_micro) + FILTER (WHERE block_time >= now() - INTERVAL '30 days') AS d30_p50, + percentile_cont(0.95) WITHIN GROUP (ORDER BY cu_price_micro) + FILTER (WHERE block_time >= now() - INTERVAL '30 days') AS d30_p95 + FROM solana_exec_cu_samples + GROUP BY platform + ) SELECT - platform, + f.platform, -- weighted averages: SUM(avg×count)/SUM(count) avoids skewing by small off-peak buckets - SUM(avg_priority_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_prio, - AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_p50, - AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_p95, - SUM(avg_platform_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_pfee, - SUM(jito_rate * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_jito, - SUM(avg_cu_consumed * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_cu, - SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_count, - - SUM(avg_priority_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_prio, - AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_p50, - AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_p95, - SUM(avg_platform_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_pfee, - SUM(jito_rate * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_jito, - SUM(avg_cu_consumed * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_cu, - SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_count, - - SUM(avg_priority_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_prio, - AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_p50, - AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_p95, - SUM(avg_platform_fee_lamports * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_pfee, - SUM(jito_rate * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_jito, - SUM(avg_cu_consumed * tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') - / NULLIF(SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_cu, - SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_count, - - MAX(bucket_start)::text AS latest_bucket - FROM solana_exec_facts - GROUP BY platform`, + SUM(f.avg_priority_fee_lamports * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_prio, + MAX(c.h24_p50) AS h24_p50, + MAX(c.h24_p95) AS h24_p95, + SUM(f.avg_platform_fee_lamports * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_pfee, + SUM(f.jito_rate * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_jito, + SUM(f.avg_cu_consumed * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_cu, + SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours') AS h24_count, + + SUM(f.avg_priority_fee_lamports * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_prio, + MAX(c.d7_p50) AS d7_p50, + MAX(c.d7_p95) AS d7_p95, + SUM(f.avg_platform_fee_lamports * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_pfee, + SUM(f.jito_rate * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_jito, + SUM(f.avg_cu_consumed * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_cu, + SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days') AS d7_count, + + SUM(f.avg_priority_fee_lamports * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_prio, + MAX(c.d30_p50) AS d30_p50, + MAX(c.d30_p95) AS d30_p95, + SUM(f.avg_platform_fee_lamports * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_pfee, + SUM(f.jito_rate * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_jito, + SUM(f.avg_cu_consumed * f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days') + / NULLIF(SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_cu, + SUM(f.tx_count) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days') AS d30_count, + + MAX(f.bucket_start)::text AS latest_bucket + FROM solana_exec_facts f + LEFT JOIN cu c USING (platform) + GROUP BY f.platform`, ) if err != nil { log.Printf("exec-api: query: %v", err) diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index ace75dbe..37287c8c 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -44,10 +44,9 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun return fmt.Errorf("get cursor: %w", err) } - const sigLimit = 100 - // Paginate through ALL signatures newer than cursor (newest-first per page). - // Each page uses `before=oldestSigInPreviousPage` to walk backwards until - // we exhaust the window. This guarantees complete coverage regardless of volume. + // Solana RPC supports up to 1000 sigs per getSignaturesForAddress call. + // Using 1000 reduces page count ~10x vs 100: 45K sigs = 45 pages instead of 450. + const sigLimit = 1000 var sigs []helius.SigEntry before := "" for { @@ -167,6 +166,17 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun return fmt.Errorf("upsert: %w", err) } + // Store raw CU price samples for true percentile computation (not AVG of hourly p50). + cuSamples := make([]store.CUSample, 0, len(events)) + for _, e := range events { + if e.CUPriceMicro > 0 { + cuSamples = append(cuSamples, store.CUSample{BlockTime: e.BlockTime, CUPriceMicro: e.CUPriceMicro}) + } + } + if err := db.InsertCUSamples(ctx, plt, cuSamples); err != nil { + return fmt.Errorf("insert cu samples: %w", err) + } + // Advance cursor to the newest sig (first in original order = last in reversed). newest := sigs[0] if err := db.SaveCursor(ctx, plt, newest.Signature, newest.Slot); err != nil { diff --git a/harnesses/solana-exec/cmd/materializer/main.go b/harnesses/solana-exec/cmd/materializer/main.go index 98fc753a..f2da3750 100644 --- a/harnesses/solana-exec/cmd/materializer/main.go +++ b/harnesses/solana-exec/cmd/materializer/main.go @@ -27,6 +27,9 @@ func main() { log.Printf("materializer: %s: done", plt) } } + if err := db.PurgeCUSamples(ctx); err != nil { + log.Printf("materializer: purge cu samples: %v", err) + } time.Sleep(5 * time.Minute) } } diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go index 1564a23d..a8746119 100644 --- a/harnesses/solana-exec/internal/store/store.go +++ b/harnesses/solana-exec/internal/store/store.go @@ -124,6 +124,40 @@ func (db *DB) UpsertRawCounts(ctx context.Context, platform string, counts map[t return tx.Commit(ctx) } +// CUSample is a single compute-unit price observation from the enhanced API sample. +type CUSample struct { + BlockTime time.Time + CUPriceMicro int64 +} + +// InsertCUSamples appends raw CU price samples; no dedup (each poll is a fresh sample). +func (db *DB) InsertCUSamples(ctx context.Context, platform string, samples []CUSample) error { + if len(samples) == 0 { + return nil + } + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("store: begin: %w", err) + } + defer tx.Rollback(ctx) + for _, s := range samples { + _, err := tx.Exec(ctx, + `INSERT INTO solana_exec_cu_samples (platform, block_time, cu_price_micro) VALUES ($1,$2,$3)`, + platform, s.BlockTime, s.CUPriceMicro, + ) + if err != nil { + return fmt.Errorf("store: insert cu sample: %w", err) + } + } + return tx.Commit(ctx) +} + +// PurgeCUSamples deletes samples older than 35 days to bound table growth. +func (db *DB) PurgeCUSamples(ctx context.Context) error { + _, err := db.pool.Exec(ctx, `DELETE FROM solana_exec_cu_samples WHERE block_time < now() - INTERVAL '35 days'`) + return err +} + // Materialize recomputes hourly facts for the given platform. // tx_count comes from solana_exec_raw_counts (full volume); fee metrics come // from the sampled solana_exec_events (representative quality metrics). diff --git a/harnesses/solana-exec/migrations/003_cu_samples.sql b/harnesses/solana-exec/migrations/003_cu_samples.sql new file mode 100644 index 00000000..38d89388 --- /dev/null +++ b/harnesses/solana-exec/migrations/003_cu_samples.sql @@ -0,0 +1,16 @@ +BEGIN; + +-- Raw CU price samples from the 100-sig enhanced API poll each cycle. +-- Gives real percentile_cont on actual data points instead of AVG(hourly_p50). +-- Retention: 35 days (purged by materializer). +CREATE TABLE IF NOT EXISTS solana_exec_cu_samples ( + id BIGSERIAL PRIMARY KEY, + platform TEXT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + cu_price_micro BIGINT NOT NULL +); + +CREATE INDEX IF NOT EXISTS solana_exec_cu_samples_platform_time + ON solana_exec_cu_samples (platform, block_time); + +COMMIT; From 9ca7cd4cc5202f92540ec1e5e9dd6c8d0036a300 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:34:28 +0200 Subject: [PATCH 09/18] fix(codex): add utls Chrome fingerprint scraper for /api/codex/token --- .../cmd/script/defined_auth.go | 12 +- .../cmd/script/utls_codex.go | 172 ++++++++++++++++++ harnesses/aggregator-head-lag/go.mod | 4 + harnesses/aggregator-head-lag/go.sum | 6 + 4 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 harnesses/aggregator-head-lag/cmd/script/utls_codex.go diff --git a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go index 987a1f13..7faeb77e 100644 --- a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go +++ b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go @@ -141,12 +141,20 @@ func GetDefinedJWTToken(sessionCookie string) (string, error) { fmt.Printf("[DEFINED-AUTH] Got token from in-process scraper (age=%v, len=%d)\n", time.Since(mintedAt).Round(time.Second), len(tok)) return tok, nil } - // Direct mint: JWE minted from this container's IP = same IP used for WS = no 4403. + // utls Chrome fingerprint scraper: visits defined.fi, gets CSRF, POSTs to /api/codex/token. + // Works if this container's IP is not in Vercel's datacenter blocklist. + if tok, err := tryUtlsCodexToken(); err == nil && tok != "" { + fmt.Printf("[DEFINED-AUTH] Got token via utls scraper (len=%d)\n", len(tok)) + return tok, nil + } else { + fmt.Printf("[DEFINED-AUTH] utls scraper failed: %v\n", err) + } + // Direct mint (standard Go TLS, usually blocked by Vercel bot check). if tok, err := tryDirectCodexToken(sessionCookie); err == nil && tok != "" { fmt.Printf("[DEFINED-AUTH] Got token via direct /api/codex/token (len=%d)\n", len(tok)) return tok, nil } - // Sidecar fallback (Paris box chromedp, auto-refreshes every 25 min — may be stale) + // Sidecar fallback (Paris box, Mac-push keeps it fresh every 5 min) if svcURL := os.Getenv("DEFINED_TOKEN_SERVICE_URL"); svcURL != "" { if tok, err := tryTokenService(svcURL); err == nil && tok != "" { fmt.Printf("[DEFINED-AUTH] Got token from sidecar (len=%d)\n", len(tok)) diff --git a/harnesses/aggregator-head-lag/cmd/script/utls_codex.go b/harnesses/aggregator-head-lag/cmd/script/utls_codex.go new file mode 100644 index 00000000..49dc46ec --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/script/utls_codex.go @@ -0,0 +1,172 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "time" + + tls "github.com/refraction-networking/utls" +) + +func chromeH1Spec() tls.ClientHelloSpec { + return tls.ClientHelloSpec{ + TLSVersMax: tls.VersionTLS13, + TLSVersMin: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.GREASE_PLACEHOLDER, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{0x00}, + Extensions: tls.ShuffleChromeTLSExtensions([]tls.TLSExtension{ + &tls.UtlsGREASEExtension{}, + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{Renegotiation: tls.RenegotiateOnceAsClient}, + &tls.SupportedCurvesExtension{[]tls.CurveID{ + tls.GREASE_PLACEHOLDER, tls.X25519, tls.CurveP256, tls.CurveP384, + }}, + &tls.SupportedPointsExtension{SupportedPoints: []byte{0x00}}, + &tls.SessionTicketExtension{}, + &tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, + &tls.StatusRequestExtension{}, + &tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, tls.PSSWithSHA256, tls.PKCS1WithSHA256, + tls.ECDSAWithP384AndSHA384, tls.PSSWithSHA384, tls.PKCS1WithSHA384, + tls.PSSWithSHA512, tls.PKCS1WithSHA512, + }}, + &tls.SCTExtension{}, + &tls.KeyShareExtension{[]tls.KeyShare{ + {Group: tls.CurveID(tls.GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: tls.X25519}, + }}, + &tls.PSKKeyExchangeModesExtension{[]uint8{tls.PskModeDHE}}, + &tls.SupportedVersionsExtension{[]uint16{ + tls.GREASE_PLACEHOLDER, tls.VersionTLS13, tls.VersionTLS12, + }}, + &tls.UtlsCompressCertExtension{[]tls.CertCompressionAlgo{tls.CertCompressionBrotli}}, + &tls.UtlsGREASEExtension{}, + &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle}, + }), + } +} + +func newUTLSClient() *http.Client { + jar, _ := cookiejar.New(nil) + return &http.Client{ + Timeout: 30 * time.Second, + Jar: jar, + Transport: &http.Transport{ + DisableKeepAlives: true, + ForceAttemptHTTP2: false, + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, _ := net.SplitHostPort(addr) + conn, err := (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + spec := chromeH1Spec() + uc := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloCustom) + if err := uc.ApplyPreset(&spec); err != nil { + conn.Close() + return nil, err + } + if err := uc.HandshakeContext(ctx); err != nil { + conn.Close() + return nil, err + } + return uc, nil + }, + }, + } +} + +// tryUtlsCodexToken uses Chrome TLS fingerprint spoofing to call /api/codex/token. +// Works from residential IPs. From datacenter IPs Vercel may return 429. +func tryUtlsCodexToken() (string, error) { + client := newUTLSClient() + + req1, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) + req1.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req1.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req1.Header.Set("Accept-Language", "en-US,en;q=0.9") + req1.Header.Set("Sec-Fetch-Site", "none") + req1.Header.Set("Sec-Fetch-Mode", "navigate") + req1.Header.Set("Sec-Fetch-Dest", "document") + req1.Header.Set("Upgrade-Insecure-Requests", "1") + + resp1, err := client.Do(req1) + if err != nil { + return "", fmt.Errorf("page load failed: %w", err) + } + io.Copy(io.Discard, resp1.Body) + resp1.Body.Close() + + if resp1.StatusCode != 200 { + return "", fmt.Errorf("page load blocked (HTTP %d) — IP blocked by Vercel", resp1.StatusCode) + } + + u, _ := url.Parse("https://www.defined.fi/") + cookies := client.Jar.Cookies(u) + var csrfToken string + for _, c := range cookies { + if c.Name == "csrf-token" { + csrfToken = c.Value + } + } + if csrfToken == "" { + return "", fmt.Errorf("no csrf-token in page cookies") + } + + req2, _ := http.NewRequest("POST", "https://www.defined.fi/api/codex/token", bytes.NewBufferString("{}")) + req2.Header.Set("Accept", "application/json") + req2.Header.Set("Content-Type", "application/json") + req2.Header.Set("x-csrf-token", csrfToken) + req2.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req2.Header.Set("Accept-Language", "en-US,en;q=0.9") + req2.Header.Set("Origin", "https://www.defined.fi") + req2.Header.Set("Referer", "https://www.defined.fi/") + req2.Header.Set("Sec-Fetch-Site", "same-origin") + req2.Header.Set("Sec-Fetch-Mode", "cors") + req2.Header.Set("Sec-Fetch-Dest", "empty") + + resp2, err := client.Do(req2) + if err != nil { + return "", fmt.Errorf("codex/token request failed: %w", err) + } + body, _ := io.ReadAll(resp2.Body) + resp2.Body.Close() + + if resp2.StatusCode != 200 { + return "", fmt.Errorf("codex/token HTTP %d: %.100s", resp2.StatusCode, string(body)) + } + + var parsed struct { + Token string `json:"token"` + } + if err := json.Unmarshal(body, &parsed); err != nil || parsed.Token == "" { + return "", fmt.Errorf("no token in response: %.100s", string(body)) + } + + return parsed.Token, nil +} diff --git a/harnesses/aggregator-head-lag/go.mod b/harnesses/aggregator-head-lag/go.mod index ed17b543..d6ccd878 100644 --- a/harnesses/aggregator-head-lag/go.mod +++ b/harnesses/aggregator-head-lag/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/andybalholm/brotli v1.0.6 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chromedp/sysutil v1.1.0 // indirect @@ -17,12 +18,15 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/refraction-networking/utls v1.8.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/sys v0.35.0 // indirect google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/harnesses/aggregator-head-lag/go.sum b/harnesses/aggregator-head-lag/go.sum index f594bc89..6379815d 100644 --- a/harnesses/aggregator-head-lag/go.sum +++ b/harnesses/aggregator-head-lag/go.sum @@ -1,3 +1,5 @@ +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -47,6 +49,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -55,6 +59,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= From bae8003ebfa8bbfacf16a46e5b77ff79d1570b90 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:38:54 +0200 Subject: [PATCH 10/18] =?UTF-8?q?feat(solana-exec):=20split=20RPC/enhanced?= =?UTF-8?q?=20API=20=E2=80=94=20public=20endpoint=20for=20pagination,=20He?= =?UTF-8?q?lius=20only=20for=20quality=20sample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- harnesses/solana-exec/cmd/collector/main.go | 6 +++++- .../solana-exec/internal/helius/client.go | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index 37287c8c..3b19b9b1 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -24,7 +24,11 @@ func main() { } defer db.Close() - h := helius.New(mustEnv("HELIUS_API_KEY")) + rpcURL := os.Getenv("SOLANA_RPC_URL") + if rpcURL == "" { + rpcURL = "https://api.mainnet-beta.solana.com" + } + h := helius.NewWithRPC(mustEnv("HELIUS_API_KEY"), rpcURL) log.Printf("collector: monitoring %d platforms", len(platform.FeeAccounts)) diff --git a/harnesses/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go index 45bc6c17..515dbdf1 100644 --- a/harnesses/solana-exec/internal/helius/client.go +++ b/harnesses/solana-exec/internal/helius/client.go @@ -10,22 +10,32 @@ import ( ) // Client wraps the Helius RPC + enhanced-transactions APIs. +// rpcURL is used for standard Solana JSON-RPC calls (getSignaturesForAddress) and can be +// any Solana endpoint — we default to the public mainnet RPC to avoid consuming Helius credits +// on raw pagination. enhURL is Helius-specific and costs credits; it is only called for the +// 100-sig quality sample per poll. type Client struct { httpClient *http.Client apiKey string - rpcURL string // https://mainnet.helius-rpc.com/?api-key=KEY - enhURL string // https://api.helius.xyz/v0/transactions?api-key=KEY + rpcURL string // standard Solana JSON-RPC (public endpoint, no credits) + enhURL string // https://api.helius.xyz/v0/transactions?api-key=KEY (credits) } -func New(apiKey string) *Client { +// NewWithRPC creates a client where rpcURL is used for standard RPC calls (getSignaturesForAddress). +// Pass a public or self-hosted endpoint to avoid consuming Helius credits on pagination. +func NewWithRPC(apiKey, rpcURL string) *Client { return &Client{ httpClient: &http.Client{Timeout: 30 * time.Second}, apiKey: apiKey, - rpcURL: fmt.Sprintf("https://mainnet.helius-rpc.com/?api-key=%s", apiKey), + rpcURL: rpcURL, enhURL: fmt.Sprintf("https://api.helius.xyz/v0/transactions?api-key=%s", apiKey), } } +func New(apiKey string) *Client { + return NewWithRPC(apiKey, "https://api.mainnet-beta.solana.com") +} + // SigEntry is one result from getSignaturesForAddress. type SigEntry struct { Signature string `json:"signature"` From fc903ca37e6bfe4935d854b817193567ec39563e Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:44:32 +0200 Subject: [PATCH 11/18] test: GitHub Actions codex token push (test Azure IPs against Vercel) --- .github/workflows/codex-push.yml | 44 ++++ .../aggregator-head-lag/cmd/test-utls/main.go | 241 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 .github/workflows/codex-push.yml create mode 100644 harnesses/aggregator-head-lag/cmd/test-utls/main.go diff --git a/.github/workflows/codex-push.yml b/.github/workflows/codex-push.yml new file mode 100644 index 00000000..bdefd9a0 --- /dev/null +++ b/.github/workflows/codex-push.yml @@ -0,0 +1,44 @@ +name: Codex token push + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + +jobs: + push-token: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: harnesses/aggregator-head-lag/go.mod + cache-dependency-path: harnesses/aggregator-head-lag/go.sum + + - name: Build scraper + working-directory: harnesses/aggregator-head-lag + run: go build -o /tmp/codex-scraper ./cmd/test-utls/ + + - name: Scrape and push token + run: | + set -euo pipefail + output=$(MODE=default /tmp/codex-scraper 2>&1) + echo "$output" | head -5 + token=$(echo "$output" | grep '^CODEX_TOKEN=' | cut -d= -f2-) + if [ -z "$token" ] || [ ${#token} -lt 100 ]; then + echo "ERROR: no token in output" + echo "$output" + exit 1 + fi + echo "Got token (len=${#token}), pushing to sidecar..." + http_code=$(curl -s -o /tmp/push_resp -w "%{http_code}" -X POST \ + -H "Content-Type: text/plain" \ + --data-raw "$token" \ + "http://57.130.19.92:8080/push") + if [ "$http_code" = "204" ]; then + echo "Push OK" + else + echo "Push failed (HTTP $http_code): $(cat /tmp/push_resp)" + exit 1 + fi diff --git a/harnesses/aggregator-head-lag/cmd/test-utls/main.go b/harnesses/aggregator-head-lag/cmd/test-utls/main.go new file mode 100644 index 00000000..2903a789 --- /dev/null +++ b/harnesses/aggregator-head-lag/cmd/test-utls/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "time" + + tls "github.com/refraction-networking/utls" +) + +func chromeH1Spec() tls.ClientHelloSpec { + return tls.ClientHelloSpec{ + TLSVersMax: tls.VersionTLS13, + TLSVersMin: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.GREASE_PLACEHOLDER, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []byte{0x00}, + Extensions: tls.ShuffleChromeTLSExtensions([]tls.TLSExtension{ + &tls.UtlsGREASEExtension{}, + &tls.SNIExtension{}, + &tls.ExtendedMasterSecretExtension{}, + &tls.RenegotiationInfoExtension{Renegotiation: tls.RenegotiateOnceAsClient}, + &tls.SupportedCurvesExtension{[]tls.CurveID{ + tls.GREASE_PLACEHOLDER, tls.X25519, tls.CurveP256, tls.CurveP384, + }}, + &tls.SupportedPointsExtension{SupportedPoints: []byte{0x00}}, + &tls.SessionTicketExtension{}, + &tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, + &tls.StatusRequestExtension{}, + &tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{ + tls.ECDSAWithP256AndSHA256, tls.PSSWithSHA256, tls.PKCS1WithSHA256, + tls.ECDSAWithP384AndSHA384, tls.PSSWithSHA384, tls.PKCS1WithSHA384, + tls.PSSWithSHA512, tls.PKCS1WithSHA512, + }}, + &tls.SCTExtension{}, + &tls.KeyShareExtension{[]tls.KeyShare{ + {Group: tls.CurveID(tls.GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: tls.X25519}, + }}, + &tls.PSKKeyExchangeModesExtension{[]uint8{tls.PskModeDHE}}, + &tls.SupportedVersionsExtension{[]uint16{ + tls.GREASE_PLACEHOLDER, tls.VersionTLS13, tls.VersionTLS12, + }}, + &tls.UtlsCompressCertExtension{[]tls.CertCompressionAlgo{tls.CertCompressionBrotli}}, + &tls.UtlsGREASEExtension{}, + &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle}, + }), + } +} + +func newUTLSClient() *http.Client { + jar, _ := cookiejar.New(nil) + return &http.Client{ + Timeout: 30 * time.Second, + Jar: jar, + Transport: &http.Transport{ + DisableKeepAlives: true, + ForceAttemptHTTP2: false, + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, _ := net.SplitHostPort(addr) + conn, err := (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + spec := chromeH1Spec() + uc := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloCustom) + if err := uc.ApplyPreset(&spec); err != nil { + conn.Close() + return nil, err + } + if err := uc.HandshakeContext(ctx); err != nil { + conn.Close() + return nil, err + } + return uc, nil + }, + }, + } +} + +func main() { + mode := os.Getenv("MODE") + + switch mode { + case "scrape": + // Step 1 only: GET defined.fi, print cookies + client := newUTLSClient() + req, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Sec-Fetch-Site", "none") + req.Header.Set("Sec-Fetch-Mode", "navigate") + req.Header.Set("Sec-Fetch-Dest", "document") + req.Header.Set("Upgrade-Insecure-Requests", "1") + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + if resp.StatusCode != 200 { + fmt.Fprintf(os.Stderr, "ERROR: HTTP %d\n", resp.StatusCode) + os.Exit(1) + } + u, _ := url.Parse("https://www.defined.fi/") + for _, c := range client.Jar.Cookies(u) { + fmt.Printf("%s=%s\n", c.Name, c.Value) + } + + case "mint-utls": + // Step 2 only via utls: POST /api/codex/token with pre-obtained cookies + attestation := os.Getenv("ATTESTATION") + csrf := os.Getenv("CSRF") + if attestation == "" || csrf == "" { + fmt.Fprintln(os.Stderr, "Need ATTESTATION and CSRF env vars") + os.Exit(1) + } + fmt.Printf("Calling /api/codex/token via utls (Chrome fingerprint)...\n") + client := newUTLSClient() + // Inject cookies into jar + u, _ := url.Parse("https://www.defined.fi/") + client.Jar.SetCookies(u, []*http.Cookie{ + {Name: "defined-attestation-token", Value: attestation}, + {Name: "csrf-token", Value: csrf}, + }) + req, _ := http.NewRequest("POST", "https://www.defined.fi/api/codex/token", bytes.NewBufferString("{}")) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-csrf-token", csrf) + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Origin", "https://www.defined.fi") + req.Header.Set("Referer", "https://www.defined.fi/") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Dest", "empty") + resp, err := client.Do(req) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + fmt.Printf("HTTP %d\n", resp.StatusCode) + if resp.StatusCode == 200 { + var parsed struct{ Token string `json:"token"` } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + fmt.Printf("✅ Got JWE (len=%d)\n", len(parsed.Token)) + fmt.Printf("CODEX_TOKEN=%s\n", parsed.Token) + return + } + } + n := len(body) + if n > 200 { + n = 200 + } + fmt.Printf("❌ Body: %s\n", string(body[:n])) + os.Exit(1) + + default: + // Full flow: scrape + mint on this machine + client := newUTLSClient() + req1, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) + req1.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req1.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req1.Header.Set("Accept-Language", "en-US,en;q=0.9") + req1.Header.Set("Sec-Fetch-Site", "none") + req1.Header.Set("Sec-Fetch-Mode", "navigate") + req1.Header.Set("Sec-Fetch-Dest", "document") + req1.Header.Set("Upgrade-Insecure-Requests", "1") + resp1, err := client.Do(req1) + if err != nil { + fmt.Printf("Page load failed: %v\n", err) + return + } + io.Copy(io.Discard, resp1.Body) + resp1.Body.Close() + fmt.Printf("Page: HTTP %d\n", resp1.StatusCode) + u, _ := url.Parse("https://www.defined.fi/") + cookies := client.Jar.Cookies(u) + var attestation, csrf string + for _, c := range cookies { + switch c.Name { + case "defined-attestation-token": + attestation = c.Value + case "csrf-token": + csrf = c.Value + } + } + fmt.Printf("attestation len=%d, csrf len=%d\n", len(attestation), len(csrf)) + req2, _ := http.NewRequest("POST", "https://www.defined.fi/api/codex/token", bytes.NewBufferString("{}")) + req2.Header.Set("Accept", "application/json") + req2.Header.Set("Content-Type", "application/json") + req2.Header.Set("x-csrf-token", csrf) + req2.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req2.Header.Set("Origin", "https://www.defined.fi") + req2.Header.Set("Referer", "https://www.defined.fi/") + req2.Header.Set("Sec-Fetch-Site", "same-origin") + req2.Header.Set("Sec-Fetch-Mode", "cors") + resp2, err := client.Do(req2) + if err != nil { + fmt.Printf("API failed: %v\n", err) + return + } + body, _ := io.ReadAll(resp2.Body) + resp2.Body.Close() + fmt.Printf("API: HTTP %d\n", resp2.StatusCode) + if resp2.StatusCode == 200 { + var parsed struct{ Token string `json:"token"` } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + fmt.Printf("✅ JWE len=%d\n", len(parsed.Token)) + } + } + } +} From c7f76e804a1d60100c442a91b81964d15681d1f8 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:53:00 +0200 Subject: [PATCH 12/18] fix(solana-exec): idempotent raw counts, Materialize subquery, API time bound, purge cu_samples --- harnesses/solana-exec/cmd/api/main.go | 1 + harnesses/solana-exec/cmd/collector/main.go | 2 +- harnesses/solana-exec/internal/store/store.go | 24 ++++++++++--------- .../migrations/004_raw_counts_idempotent.sql | 17 +++++++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql diff --git a/harnesses/solana-exec/cmd/api/main.go b/harnesses/solana-exec/cmd/api/main.go index cccdc7a4..491b702e 100644 --- a/harnesses/solana-exec/cmd/api/main.go +++ b/harnesses/solana-exec/cmd/api/main.go @@ -121,6 +121,7 @@ func handleExecLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { MAX(f.bucket_start)::text AS latest_bucket FROM solana_exec_facts f LEFT JOIN cu c USING (platform) + WHERE f.bucket_start >= now() - INTERVAL '31 days' GROUP BY f.platform`, ) if err != nil { diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index 3b19b9b1..c5bc7a51 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -97,7 +97,7 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun bucket := time.Unix(s.BlockTime, 0).UTC().Truncate(time.Hour) hourBuckets[bucket]++ } - if err := db.UpsertRawCounts(ctx, plt, hourBuckets); err != nil { + if err := db.UpsertRawCounts(ctx, plt, cursor.LastSig, hourBuckets); err != nil { return fmt.Errorf("upsert raw counts: %w", err) } diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go index a8746119..da9cdfd0 100644 --- a/harnesses/solana-exec/internal/store/store.go +++ b/harnesses/solana-exec/internal/store/store.go @@ -98,8 +98,10 @@ func (db *DB) SaveCursor(ctx context.Context, platform, lastSig string, slot uin } // UpsertRawCounts stores hourly successful-tx counts from raw sig pagination. -// counts maps bucket_start (hour-truncated UTC) → total successful sigs that hour. -func (db *DB) UpsertRawCounts(ctx context.Context, platform string, counts map[time.Time]int64) error { +// fromCursor is cursor.LastSig at poll start — it makes each row idempotent: +// same (platform, bucket, fromCursor) on retry → DO NOTHING, no double-count. +// counts maps bucket_start (hour-truncated UTC) → successful sigs in this poll window. +func (db *DB) UpsertRawCounts(ctx context.Context, platform, fromCursor string, counts map[time.Time]int64) error { if len(counts) == 0 { return nil } @@ -110,12 +112,10 @@ func (db *DB) UpsertRawCounts(ctx context.Context, platform string, counts map[t defer tx.Rollback(ctx) for bucket, count := range counts { _, err := tx.Exec(ctx, ` - INSERT INTO solana_exec_raw_counts (platform, bucket_start, success_count, updated_at) - VALUES ($1, $2, $3, now()) - ON CONFLICT (platform, bucket_start) DO UPDATE SET - success_count = solana_exec_raw_counts.success_count + EXCLUDED.success_count, - updated_at = now()`, - platform, bucket, count, + INSERT INTO solana_exec_raw_counts (platform, bucket_start, from_cursor, success_count, updated_at) + VALUES ($1, $2, $3, $4, now()) + ON CONFLICT (platform, bucket_start, from_cursor) DO NOTHING`, + platform, bucket, fromCursor, count, ) if err != nil { return fmt.Errorf("store: upsert raw count %v: %w", bucket, err) @@ -179,9 +179,11 @@ func (db *DB) Materialize(ctx context.Context, platform string) error { AVG(e.cu_consumed) AS avg_cu_consumed, now() FROM solana_exec_events e - LEFT JOIN solana_exec_raw_counts r - ON r.platform = e.platform - AND r.bucket_start = date_trunc('hour', e.block_time) + LEFT JOIN ( + SELECT platform, bucket_start, SUM(success_count) AS success_count + FROM solana_exec_raw_counts + GROUP BY platform, bucket_start + ) r ON r.platform = e.platform AND r.bucket_start = date_trunc('hour', e.block_time) WHERE e.platform = $1 GROUP BY e.platform, date_trunc('hour', e.block_time), r.success_count ON CONFLICT (platform, bucket_start) DO UPDATE SET diff --git a/harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql b/harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql new file mode 100644 index 00000000..66ab942a --- /dev/null +++ b/harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql @@ -0,0 +1,17 @@ +BEGIN; + +-- Make raw_counts idempotent on crash/restart. +-- Problem: UpsertRawCounts accumulated (success_count + EXCLUDED.success_count). +-- On a crash between UpsertRawCounts and SaveCursor, the next boot re-processes +-- the same sigs and double-counts. Fix: key by (platform, bucket_start, from_cursor) +-- so each poll's contribution is a distinct row. ON CONFLICT DO NOTHING = fully idempotent. + +ALTER TABLE solana_exec_raw_counts DROP CONSTRAINT solana_exec_raw_counts_pkey; + +-- from_cursor = cursor.LastSig at the start of the poll that produced this row. +-- Existing rows get '' which is safe — they predate this schema change and won't be re-inserted. +ALTER TABLE solana_exec_raw_counts ADD COLUMN IF NOT EXISTS from_cursor TEXT NOT NULL DEFAULT ''; + +ALTER TABLE solana_exec_raw_counts ADD PRIMARY KEY (platform, bucket_start, from_cursor); + +COMMIT; From ed4e5e16b018f461c1ae2e91114ab830b6ee35d2 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:04:24 +0200 Subject: [PATCH 13/18] fix(solana-exec): retry GetSignaturesForAddress on 429 with backoff --- .../solana-exec/internal/helius/client.go | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/harnesses/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go index 515dbdf1..29433118 100644 --- a/harnesses/solana-exec/internal/helius/client.go +++ b/harnesses/solana-exec/internal/helius/client.go @@ -67,36 +67,57 @@ func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, li "params": []any{address, params}, }) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.rpcURL, bytes.NewReader(body)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("helius RPC HTTP %d", resp.StatusCode) - } - - var out struct { - Result []SigEntry `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return nil, fmt.Errorf("helius RPC decode: %w", err) - } - if out.Error != nil { - return nil, fmt.Errorf("helius RPC error %d: %s", out.Error.Code, out.Error.Message) - } - return out.Result, nil + // Retry up to 3 times on 429 or transient errors with exponential backoff. + var lastErr error + for attempt := range 3 { + if attempt > 0 { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(time.Duration(attempt*attempt) * time.Second): + } + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.rpcURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + lastErr = err + continue + } + + if resp.StatusCode == http.StatusTooManyRequests { + resp.Body.Close() + lastErr = fmt.Errorf("helius RPC HTTP 429") + continue + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("helius RPC HTTP %d", resp.StatusCode) + } + + var out struct { + Result []SigEntry `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + resp.Body.Close() + return nil, fmt.Errorf("helius RPC decode: %w", err) + } + resp.Body.Close() + if out.Error != nil { + return nil, fmt.Errorf("helius RPC error %d: %s", out.Error.Code, out.Error.Message) + } + return out.Result, nil + } + return nil, fmt.Errorf("helius RPC: %w (after 3 attempts)", lastErr) } // NativeTransfer is a SOL transfer extracted by Helius. From 1d5826320aa3c3b642ef3d195c53f918eb938c3a Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:12:27 +0200 Subject: [PATCH 14/18] fix: use un.defined.fi legacy API for codex JWE minting un.defined.fi/api createApiTokens still returns valid JWE tokens without requiring session cookies or CSRF. Try this first via utls, fall back to www.defined.fi/api/codex/token (needs page visit + CSRF). generateDefinedJWTToken also switched to un.defined.fi. --- .../cmd/script/defined_auth.go | 54 +++++++--------- .../cmd/script/utls_codex.go | 63 +++++++++++++++++-- .../aggregator-head-lag/cmd/test-utls/main.go | 20 ++++++ 3 files changed, 98 insertions(+), 39 deletions(-) diff --git a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go index 7faeb77e..c38dfd56 100644 --- a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go +++ b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go @@ -207,49 +207,39 @@ func GetDefinedJWTToken(sessionCookie string) (string, error) { return token, nil } -// generateDefinedJWTToken generates a new JWT token from Defined.fi session cookie -func generateDefinedJWTToken(sessionCookie string) (string, error) { - fmt.Println("[DEFINED-AUTH] Generating new JWT token from Defined.fi (local)...") - fmt.Println("[DEFINED-AUTH] Creating new HTTP client with fresh TCP connection (no keepalive)") - - // Create a new HTTP client with fresh connection for each request. - // CRITICAL: route through HTTP_PROXY/HTTPS_PROXY (webshare rotating proxy) - // so each JWT mint hits a fresh IP. Direct from the container IP gets us - // stuck on Vercel's bot ban (429 loop) when the container restarts often. - transport := &http.Transport{ - DisableKeepAlives: true, - MaxIdleConnsPerHost: 0, - Proxy: http.ProxyFromEnvironment, - } - client := &http.Client{ - Timeout: 10 * time.Second, - Transport: transport, - } +// generateDefinedJWTToken generates a new JWT token via un.defined.fi (old UI, still alive). +// No session cookie or CSRF needed. Falls back to www.defined.fi/api proxy path. +func generateDefinedJWTToken(_ string) (string, error) { + fmt.Println("[DEFINED-AUTH] Generating token via un.defined.fi legacy API...") reqBody := map[string]interface{}{ "operationName": "CreateApiToken", "query": "mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }", "variables": map[string]interface{}{}, } - bodyBytes, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", "https://www.defined.fi/api", bytes.NewBuffer(bodyBytes)) + transport := &http.Transport{ + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + } + client := &http.Client{Timeout: 10 * time.Second, Transport: transport} + + req, _ := http.NewRequest("POST", "https://un.defined.fi/api", bytes.NewBuffer(bodyBytes)) req.Header.Set("Accept", "application/json") - req.Header.Set("Accept-Language", "en-US,en;q=0.9") req.Header.Set("Content-Type", "application/json") - req.Header.Set("Origin", "https://www.defined.fi") - req.Header.Set("Referer", "https://www.defined.fi/") req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Origin", "https://un.defined.fi") + req.Header.Set("Referer", "https://un.defined.fi/") req.Header.Set("sec-ch-ua", `"Not_A Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"`) req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("sec-ch-ua-platform", `"macOS"`) req.Header.Set("sec-fetch-dest", "empty") req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-site", "same-origin") - req.AddCookie(&http.Cookie{Name: "defined-attestation-token", Value: sessionCookie}) - fmt.Println("[DEFINED-AUTH] Sending POST request to https://www.defined.fi/api...") + fmt.Println("[DEFINED-AUTH] POST https://un.defined.fi/api ...") resp, err := client.Do(req) if err != nil { fmt.Printf("[DEFINED-AUTH] ❌ Request failed: %v\n", err) @@ -261,18 +251,16 @@ func generateDefinedJWTToken(sessionCookie string) (string, error) { fmt.Printf("[DEFINED-AUTH] Response status: %d\n", resp.StatusCode) if resp.StatusCode == 429 { - // Parse retry-after header if available retryAfter := resp.Header.Get("Retry-After") fmt.Printf("[DEFINED-AUTH] ⚠ Rate limited! Retry-After: %s\n", retryAfter) - if retryAfter != "" { - return "", fmt.Errorf("rate limited (429), retry after: %s", retryAfter) - } - return "", fmt.Errorf("rate limited (429), too many token requests - will retry later") + return "", fmt.Errorf("rate limited (429)") } if resp.StatusCode != 200 { - fmt.Printf("[DEFINED-AUTH] ❌ Unexpected status %d: %s\n", resp.StatusCode, string(respBody[:min(len(respBody), 100)])) - return "", fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody[:min(len(respBody), 100)])) + n := len(respBody) + if n > 100 { n = 100 } + fmt.Printf("[DEFINED-AUTH] ❌ Unexpected status %d: %s\n", resp.StatusCode, string(respBody[:n])) + return "", fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody[:n])) } var tokenResp DefinedTokenResponse @@ -286,7 +274,7 @@ func generateDefinedJWTToken(sessionCookie string) (string, error) { return "", fmt.Errorf("no token returned") } - fmt.Printf("[DEFINED-AUTH] ✅ JWT token generated successfully (length: %d)\n", len(tokenResp.Data.CreateApiTokens[0].Token)) + fmt.Printf("[DEFINED-AUTH] ✅ Token generated via un.defined.fi (length: %d)\n", len(tokenResp.Data.CreateApiTokens[0].Token)) return tokenResp.Data.CreateApiTokens[0].Token, nil } diff --git a/harnesses/aggregator-head-lag/cmd/script/utls_codex.go b/harnesses/aggregator-head-lag/cmd/script/utls_codex.go index 49dc46ec..a323bb64 100644 --- a/harnesses/aggregator-head-lag/cmd/script/utls_codex.go +++ b/harnesses/aggregator-head-lag/cmd/script/utls_codex.go @@ -101,9 +101,60 @@ func newUTLSClient() *http.Client { } } -// tryUtlsCodexToken uses Chrome TLS fingerprint spoofing to call /api/codex/token. -// Works from residential IPs. From datacenter IPs Vercel may return 429. +// tryUtlsLegacyAPI calls un.defined.fi/api (old UI, still alive) with createApiTokens mutation. +// No cookies or CSRF needed — one request, much simpler. Works from residential IPs. +// Try this first since it's a single POST with no page-visit prerequisite. +func tryUtlsLegacyAPI() (string, error) { + client := newUTLSClient() + + body, _ := json.Marshal(map[string]interface{}{ + "operationName": "CreateApiToken", + "query": "mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }", + "variables": map[string]interface{}{}, + }) + req, _ := http.NewRequest("POST", "https://un.defined.fi/api", bytes.NewBuffer(body)) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Origin", "https://un.defined.fi") + req.Header.Set("Referer", "https://un.defined.fi/") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Dest", "empty") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("request failed: %w", err) + } + respBody, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP %d: %.100s", resp.StatusCode, string(respBody)) + } + + var parsed struct { + Data struct { + CreateApiTokens []struct { + Token string `json:"token"` + } `json:"createApiTokens"` + } `json:"data"` + } + if err := json.Unmarshal(respBody, &parsed); err != nil || len(parsed.Data.CreateApiTokens) == 0 || parsed.Data.CreateApiTokens[0].Token == "" { + return "", fmt.Errorf("no token in response: %.100s", string(respBody)) + } + return parsed.Data.CreateApiTokens[0].Token, nil +} + +// tryUtlsCodexToken uses Chrome TLS fingerprint spoofing to call www.defined.fi/api/codex/token. +// Requires a page visit first to get CSRF cookie. Fallback if tryUtlsLegacyAPI fails. func tryUtlsCodexToken() (string, error) { + // Try the old un.defined.fi API first — no cookies or CSRF needed. + if tok, err := tryUtlsLegacyAPI(); err == nil && tok != "" { + return tok, nil + } + client := newUTLSClient() req1, _ := http.NewRequest("GET", "https://www.defined.fi/", nil) @@ -154,18 +205,18 @@ func tryUtlsCodexToken() (string, error) { if err != nil { return "", fmt.Errorf("codex/token request failed: %w", err) } - body, _ := io.ReadAll(resp2.Body) + body2, _ := io.ReadAll(resp2.Body) resp2.Body.Close() if resp2.StatusCode != 200 { - return "", fmt.Errorf("codex/token HTTP %d: %.100s", resp2.StatusCode, string(body)) + return "", fmt.Errorf("codex/token HTTP %d: %.100s", resp2.StatusCode, string(body2)) } var parsed struct { Token string `json:"token"` } - if err := json.Unmarshal(body, &parsed); err != nil || parsed.Token == "" { - return "", fmt.Errorf("no token in response: %.100s", string(body)) + if err := json.Unmarshal(body2, &parsed); err != nil || parsed.Token == "" { + return "", fmt.Errorf("no token in response: %.100s", string(body2)) } return parsed.Token, nil diff --git a/harnesses/aggregator-head-lag/cmd/test-utls/main.go b/harnesses/aggregator-head-lag/cmd/test-utls/main.go index 2903a789..cbb8adbc 100644 --- a/harnesses/aggregator-head-lag/cmd/test-utls/main.go +++ b/harnesses/aggregator-head-lag/cmd/test-utls/main.go @@ -106,6 +106,26 @@ func main() { mode := os.Getenv("MODE") switch mode { + case "fetch-html": + // Fetch URL via utls, print body to stdout + target := os.Getenv("URL") + if target == "" { + target = "https://www.defined.fi/" + } + client := newUTLSClient() + req, _ := http.NewRequest("GET", target, nil) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + fmt.Fprintf(os.Stderr, "HTTP %d\n", resp.StatusCode) + io.Copy(os.Stdout, resp.Body) + case "scrape": // Step 1 only: GET defined.fi, print cookies client := newUTLSClient() From 779ef72535570bf41889ae501d583c659cd94bcf Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:15:35 +0200 Subject: [PATCH 15/18] fix(solana-exec): idempotent cu_samples by sig, purge events 90d, purge cu_samples in materializer --- harnesses/solana-exec/cmd/collector/main.go | 2 +- .../solana-exec/cmd/materializer/main.go | 3 +++ harnesses/solana-exec/internal/store/store.go | 24 ++++++++++++++----- .../005_events_purge_and_cu_sig.sql | 18 ++++++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 harnesses/solana-exec/migrations/005_events_purge_and_cu_sig.sql diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index c5bc7a51..97fa6484 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -174,7 +174,7 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccoun cuSamples := make([]store.CUSample, 0, len(events)) for _, e := range events { if e.CUPriceMicro > 0 { - cuSamples = append(cuSamples, store.CUSample{BlockTime: e.BlockTime, CUPriceMicro: e.CUPriceMicro}) + cuSamples = append(cuSamples, store.CUSample{Sig: e.Sig, BlockTime: e.BlockTime, CUPriceMicro: e.CUPriceMicro}) } } if err := db.InsertCUSamples(ctx, plt, cuSamples); err != nil { diff --git a/harnesses/solana-exec/cmd/materializer/main.go b/harnesses/solana-exec/cmd/materializer/main.go index f2da3750..63e0c462 100644 --- a/harnesses/solana-exec/cmd/materializer/main.go +++ b/harnesses/solana-exec/cmd/materializer/main.go @@ -30,6 +30,9 @@ func main() { if err := db.PurgeCUSamples(ctx); err != nil { log.Printf("materializer: purge cu samples: %v", err) } + if err := db.PurgeEvents(ctx); err != nil { + log.Printf("materializer: purge events: %v", err) + } time.Sleep(5 * time.Minute) } } diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go index da9cdfd0..be6eeb53 100644 --- a/harnesses/solana-exec/internal/store/store.go +++ b/harnesses/solana-exec/internal/store/store.go @@ -126,11 +126,13 @@ func (db *DB) UpsertRawCounts(ctx context.Context, platform, fromCursor string, // CUSample is a single compute-unit price observation from the enhanced API sample. type CUSample struct { - BlockTime time.Time - CUPriceMicro int64 + Sig string + BlockTime time.Time + CUPriceMicro int64 } -// InsertCUSamples appends raw CU price samples; no dedup (each poll is a fresh sample). +// InsertCUSamples stores CU price samples idempotently keyed on (platform, sig). +// Crash-safe: retry after failure re-inserts the same sigs → ON CONFLICT DO NOTHING. func (db *DB) InsertCUSamples(ctx context.Context, platform string, samples []CUSample) error { if len(samples) == 0 { return nil @@ -142,11 +144,13 @@ func (db *DB) InsertCUSamples(ctx context.Context, platform string, samples []CU defer tx.Rollback(ctx) for _, s := range samples { _, err := tx.Exec(ctx, - `INSERT INTO solana_exec_cu_samples (platform, block_time, cu_price_micro) VALUES ($1,$2,$3)`, - platform, s.BlockTime, s.CUPriceMicro, + `INSERT INTO solana_exec_cu_samples (platform, sig, block_time, cu_price_micro) + VALUES ($1,$2,$3,$4) + ON CONFLICT (platform, sig) DO NOTHING`, + platform, s.Sig, s.BlockTime, s.CUPriceMicro, ) if err != nil { - return fmt.Errorf("store: insert cu sample: %w", err) + return fmt.Errorf("store: insert cu sample %s: %w", s.Sig, err) } } return tx.Commit(ctx) @@ -158,6 +162,14 @@ func (db *DB) PurgeCUSamples(ctx context.Context) error { return err } +// PurgeEvents deletes sampled events older than 90 days. +// Facts table already holds aggregated metrics; raw events beyond this window +// only cost storage without adding analytical value. +func (db *DB) PurgeEvents(ctx context.Context) error { + _, err := db.pool.Exec(ctx, `DELETE FROM solana_exec_events WHERE block_time < now() - INTERVAL '90 days'`) + return err +} + // Materialize recomputes hourly facts for the given platform. // tx_count comes from solana_exec_raw_counts (full volume); fee metrics come // from the sampled solana_exec_events (representative quality metrics). diff --git a/harnesses/solana-exec/migrations/005_events_purge_and_cu_sig.sql b/harnesses/solana-exec/migrations/005_events_purge_and_cu_sig.sql new file mode 100644 index 00000000..2594ba92 --- /dev/null +++ b/harnesses/solana-exec/migrations/005_events_purge_and_cu_sig.sql @@ -0,0 +1,18 @@ +BEGIN; + +-- Bound events table growth: materializer will purge rows older than 90 days. +-- Facts table already holds all aggregated metrics; raw events are only needed +-- for re-materialization within the rolling window. +-- No schema change needed — purge is done in application code. + +-- Make cu_samples idempotent on crash/restart by keying on (platform, sig). +-- Without this, a crash between UpsertEvents and SaveCursor causes the next +-- boot to re-call InsertCUSamples for the same 100 sigs, duplicating them +-- and slightly skewing percentiles. +ALTER TABLE solana_exec_cu_samples ADD COLUMN IF NOT EXISTS sig TEXT; + +CREATE UNIQUE INDEX IF NOT EXISTS solana_exec_cu_samples_platform_sig + ON solana_exec_cu_samples (platform, sig) + WHERE sig IS NOT NULL; + +COMMIT; From 50477a595687f81725ec28768e5d94b0725c397f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:52:26 +0200 Subject: [PATCH 16/18] feat: add edge relay for codex token minting --- src/app/api/internal/codex-token/route.ts | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/app/api/internal/codex-token/route.ts diff --git a/src/app/api/internal/codex-token/route.ts b/src/app/api/internal/codex-token/route.ts new file mode 100644 index 00000000..c921b979 --- /dev/null +++ b/src/app/api/internal/codex-token/route.ts @@ -0,0 +1,68 @@ +export const runtime = 'edge'; + +const SECRET = process.env.INTERNAL_RELAY_SECRET ?? ''; + +export async function GET(request: Request) { + const auth = request.headers.get('x-relay-secret'); + if (SECRET && auth !== SECRET) { + return new Response('Forbidden', { status: 403 }); + } + + const body = JSON.stringify({ + operationName: 'CreateApiToken', + query: 'mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }', + variables: {}, + }); + + const resp = await fetch('https://un.defined.fi/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Origin: 'https://un.defined.fi', + Referer: 'https://un.defined.fi/', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + }, + body, + }); + + const status = resp.status; + const text = await resp.text(); + + if (status !== 200) { + return new Response(JSON.stringify({ error: `upstream ${status}`, body: text.slice(0, 200) }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } + + let parsed: { data?: { createApiTokens?: { token: string }[] } }; + try { + parsed = JSON.parse(text); + } catch { + return new Response(JSON.stringify({ error: 'parse error', body: text.slice(0, 200) }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const token = parsed?.data?.createApiTokens?.[0]?.token; + if (!token) { + return new Response(JSON.stringify({ error: 'no token', body: text.slice(0, 300) }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(token, { + headers: { 'Content-Type': 'text/plain' }, + }); +} From 182c4f7253e87728d0978f4f36ed4d75d27a8e12 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:57:32 +0200 Subject: [PATCH 17/18] feat: utls+Tor SOCKS5 for Codex token via Paris box pm2 --- .github/workflows/codex-push.yml | 37 +--- .../aggregator-head-lag/cmd/test-utls/main.go | 158 ++++++++++++++++-- harnesses/aggregator-head-lag/go.mod | 9 +- harnesses/aggregator-head-lag/go.sum | 10 +- 4 files changed, 160 insertions(+), 54 deletions(-) diff --git a/.github/workflows/codex-push.yml b/.github/workflows/codex-push.yml index bdefd9a0..f863b999 100644 --- a/.github/workflows/codex-push.yml +++ b/.github/workflows/codex-push.yml @@ -9,36 +9,17 @@ jobs: push-token: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version-file: harnesses/aggregator-head-lag/go.mod - cache-dependency-path: harnesses/aggregator-head-lag/go.sum - - - name: Build scraper - working-directory: harnesses/aggregator-head-lag - run: go build -o /tmp/codex-scraper ./cmd/test-utls/ - - - name: Scrape and push token + - name: Trigger Paris box token refresh run: | set -euo pipefail - output=$(MODE=default /tmp/codex-scraper 2>&1) - echo "$output" | head -5 - token=$(echo "$output" | grep '^CODEX_TOKEN=' | cut -d= -f2-) - if [ -z "$token" ] || [ ${#token} -lt 100 ]; then - echo "ERROR: no token in output" - echo "$output" - exit 1 - fi - echo "Got token (len=${#token}), pushing to sidecar..." - http_code=$(curl -s -o /tmp/push_resp -w "%{http_code}" -X POST \ - -H "Content-Type: text/plain" \ - --data-raw "$token" \ - "http://57.130.19.92:8080/push") - if [ "$http_code" = "204" ]; then - echo "Push OK" + # The Paris box pm2 "codex-push" job handles token refresh automatically + # via Tor+utls every 8 minutes. This workflow just verifies the sidecar is alive. + http_code=$(curl -s -o /tmp/tok -w "%{http_code}" --max-time 10 \ + "http://57.130.19.92:8080/token") + len=$(wc -c < /tmp/tok) + if [ "$http_code" = "200" ] && [ "$len" -gt 100 ]; then + echo "Sidecar OK: HTTP $http_code, token len=$len" else - echo "Push failed (HTTP $http_code): $(cat /tmp/push_resp)" + echo "Sidecar unhealthy: HTTP $http_code, len=$len" exit 1 fi diff --git a/harnesses/aggregator-head-lag/cmd/test-utls/main.go b/harnesses/aggregator-head-lag/cmd/test-utls/main.go index cbb8adbc..fdfeb4f8 100644 --- a/harnesses/aggregator-head-lag/cmd/test-utls/main.go +++ b/harnesses/aggregator-head-lag/cmd/test-utls/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -11,9 +12,11 @@ import ( "net/http/cookiejar" "net/url" "os" + "strings" "time" tls "github.com/refraction-networking/utls" + "golang.org/x/net/proxy" ) func chromeH1Spec() tls.ClientHelloSpec { @@ -73,35 +76,107 @@ func chromeH1Spec() tls.ClientHelloSpec { } func newUTLSClient() *http.Client { + return newUTLSClientWithProxy("") +} + +func newUTLSClientWithProxy(proxyURL string) *http.Client { jar, _ := cookiejar.New(nil) + dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, _ := net.SplitHostPort(addr) + var rawConn net.Conn + var err error + if proxyURL != "" { + u, _ := url.Parse(proxyURL) + if u.Scheme == "socks5" { + // SOCKS5 proxy (e.g. Tor) + rawConn, err = dialViaSOCKS5(ctx, proxyURL, addr) + } else { + // HTTP CONNECT proxy + rawConn, err = dialViaProxy(ctx, proxyURL, addr) + } + } else { + rawConn, err = (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, network, addr) + } + if err != nil { + return nil, err + } + spec := chromeH1Spec() + uc := tls.UClient(rawConn, &tls.Config{ServerName: host}, tls.HelloCustom) + if err := uc.ApplyPreset(&spec); err != nil { + rawConn.Close() + return nil, err + } + if err := uc.HandshakeContext(ctx); err != nil { + rawConn.Close() + return nil, err + } + return uc, nil + } return &http.Client{ Timeout: 30 * time.Second, Jar: jar, Transport: &http.Transport{ DisableKeepAlives: true, ForceAttemptHTTP2: false, - DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - host, _, _ := net.SplitHostPort(addr) - conn, err := (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, network, addr) - if err != nil { - return nil, err - } - spec := chromeH1Spec() - uc := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloCustom) - if err := uc.ApplyPreset(&spec); err != nil { - conn.Close() - return nil, err - } - if err := uc.HandshakeContext(ctx); err != nil { - conn.Close() - return nil, err - } - return uc, nil - }, + DialTLSContext: dialTLS, }, } } +// dialViaSOCKS5 dials through a SOCKS5 proxy (e.g. Tor at socks5://127.0.0.1:9050). +func dialViaSOCKS5(ctx context.Context, proxyURL string, targetAddr string) (net.Conn, error) { + u, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("bad socks5 URL: %w", err) + } + var auth *proxy.Auth + if u.User != nil { + pass, _ := u.User.Password() + auth = &proxy.Auth{User: u.User.Username(), Password: pass} + } + dialer, err := proxy.SOCKS5("tcp", u.Host, auth, &net.Dialer{Timeout: 15 * time.Second}) + if err != nil { + return nil, fmt.Errorf("socks5 dialer: %w", err) + } + return dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", targetAddr) +} + +// dialViaProxy opens an HTTP CONNECT tunnel to addr through a proxy. +func dialViaProxy(ctx context.Context, proxyAddr, targetAddr string) (net.Conn, error) { + u, err := url.Parse(proxyAddr) + if err != nil { + return nil, fmt.Errorf("bad proxy URL: %w", err) + } + proxyHost := u.Host + conn, err := (&net.Dialer{Timeout: 15 * time.Second}).DialContext(ctx, "tcp", proxyHost) + if err != nil { + return nil, fmt.Errorf("dial proxy: %w", err) + } + req := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", targetAddr, targetAddr) + if u.User != nil { + pass, _ := u.User.Password() + creds := base64.StdEncoding.EncodeToString([]byte(u.User.Username() + ":" + pass)) + req += "Proxy-Authorization: Basic " + creds + "\r\n" + } + req += "\r\n" + if _, err := conn.Write([]byte(req)); err != nil { + conn.Close() + return nil, fmt.Errorf("proxy CONNECT write: %w", err) + } + buf := make([]byte, 256) + n, err := conn.Read(buf) + if err != nil { + conn.Close() + return nil, fmt.Errorf("proxy CONNECT read: %w", err) + } + resp := string(buf[:n]) + if !strings.HasPrefix(resp, "HTTP/1.1 200") && !strings.HasPrefix(resp, "HTTP/1.0 200") { + conn.Close() + return nil, fmt.Errorf("proxy CONNECT failed: %s", strings.TrimSpace(resp)) + } + return conn, nil +} + func main() { mode := os.Getenv("MODE") @@ -257,5 +332,52 @@ func main() { fmt.Printf("✅ JWE len=%d\n", len(parsed.Token)) } } + + case "proxy-legacy": + // POST to un.defined.fi/api via utls + CONNECT proxy + proxy := os.Getenv("PROXY") + if proxy == "" { + proxy = "http://ahamwkse-rotate:rxlan97jaffe@p.webshare.io:80" + } + fmt.Fprintf(os.Stderr, "Proxy: %s\n", proxy) + client := newUTLSClientWithProxy(proxy) + body, _ := json.Marshal(map[string]interface{}{ + "operationName": "CreateApiToken", + "query": "mutation CreateApiToken { createApiTokens(input: { count: 1 }) { token } }", + "variables": map[string]interface{}{}, + }) + req, _ := http.NewRequest("POST", "https://un.defined.fi/api", bytes.NewBuffer(body)) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://un.defined.fi") + req.Header.Set("Referer", "https://un.defined.fi/") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + respBody, _ := io.ReadAll(resp.Body) + resp.Body.Close() + fmt.Fprintf(os.Stderr, "HTTP %d\n", resp.StatusCode) + if resp.StatusCode == 200 { + var parsed struct { + Data struct { + CreateApiTokens []struct{ Token string `json:"token"` } `json:"createApiTokens"` + } `json:"data"` + } + if err := json.Unmarshal(respBody, &parsed); err == nil && len(parsed.Data.CreateApiTokens) > 0 && parsed.Data.CreateApiTokens[0].Token != "" { + tok := parsed.Data.CreateApiTokens[0].Token + fmt.Printf("✅ Got JWE (len=%d)\nCODEX_TOKEN=%s\n", len(tok), tok) + os.Exit(0) + } + } + n := len(respBody) + if n > 300 { + n = 300 + } + fmt.Fprintf(os.Stderr, "Body: %s\n", string(respBody[:n])) + os.Exit(1) } } diff --git a/harnesses/aggregator-head-lag/go.mod b/harnesses/aggregator-head-lag/go.mod index d6ccd878..0265c5d2 100644 --- a/harnesses/aggregator-head-lag/go.mod +++ b/harnesses/aggregator-head-lag/go.mod @@ -1,12 +1,14 @@ module mobula_latency_competitor -go 1.24.4 +go 1.25.0 require ( github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 github.com/chromedp/chromedp v0.14.2 github.com/gorilla/websocket v1.5.3 github.com/prometheus/client_golang v1.23.2 + github.com/refraction-networking/utls v1.8.2 + golang.org/x/net v0.57.0 ) require ( @@ -24,9 +26,8 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/refraction-networking/utls v1.8.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/sys v0.47.0 // indirect google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/harnesses/aggregator-head-lag/go.sum b/harnesses/aggregator-head-lag/go.sum index 6379815d..b4a5c274 100644 --- a/harnesses/aggregator-head-lag/go.sum +++ b/harnesses/aggregator-head-lag/go.sum @@ -59,11 +59,13 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 2fa700ab7dcd5c5d053f12bf1fc146542f366ffc Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:06:44 +0200 Subject: [PATCH 18/18] fix(codex-scraper): cap at 3 failures then disable to stop log spam --- .../aggregator-head-lag/cmd/script/codex_scraper.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go index 41740bd4..2033448f 100644 --- a/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go +++ b/harnesses/aggregator-head-lag/cmd/script/codex_scraper.go @@ -120,7 +120,7 @@ func chromeAvailable() bool { } // startInProcessScraper launches a background goroutine that refreshes the JWE every 5 min. -// Call once from main. No-ops silently if Chrome is not installed. +// Call once from main. No-ops silently if Chrome is not installed or repeatedly fails. func startInProcessScraper(stopChan <-chan struct{}) { if !chromeAvailable() { fmt.Println("[CODEX-SCRAPER] Chrome not found — in-process scraper disabled (sidecar will be used)") @@ -128,7 +128,6 @@ func startInProcessScraper(stopChan <-chan struct{}) { } go func() { - // Initial delay: let the container fully start before launching Chrome. select { case <-stopChan: return @@ -136,12 +135,19 @@ func startInProcessScraper(stopChan <-chan struct{}) { } refreshInterval := 5 * time.Minute + consecutiveFails := 0 + const maxFails = 3 for { fmt.Println("[CODEX-SCRAPER] Scraping defined.fi for fresh JWE...") tok, err := scrapeCodexToken() if err != nil { - fmt.Printf("[CODEX-SCRAPER] Scrape failed: %v — retrying in 60s\n", err) + consecutiveFails++ + if consecutiveFails >= maxFails { + fmt.Printf("[CODEX-SCRAPER] %d consecutive failures — disabling scraper (sidecar will be used)\n", maxFails) + return + } + fmt.Printf("[CODEX-SCRAPER] Scrape failed (%d/%d): %v — retrying in 60s\n", consecutiveFails, maxFails, err) select { case <-stopChan: return @@ -149,6 +155,7 @@ func startInProcessScraper(stopChan <-chan struct{}) { continue } } + consecutiveFails = 0 setInProcessJWE(tok) fmt.Printf("[CODEX-SCRAPER] Fresh JWE stored (len=%d), next refresh in %v\n", len(tok), refreshInterval)