From f46ff2cfa30ac0c28874dfef37bb355474e3c764 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 24 Jul 2026 17:29:11 +0200 Subject: [PATCH] drop token-trade-coverage bench 090 Self-baseline bias: Mobula pinned at 100% because it returned the highest raw trade counts (more DEXs + counts route splits). Not apple-to-apple. Future rebuild needs RPC-derived ground truth. Spec + harness removed, slug added to REMOVED_BENCH_SLUGS (410 on prod). Bumped bench-unfiltered v43->v44 + all-benchmarks v46->v47. --- benchmarks/token-trade-coverage.yml | 131 ------------ harnesses/token-trade-coverage/.env.example | 41 ---- harnesses/token-trade-coverage/.gitignore | 2 - harnesses/token-trade-coverage/Dockerfile | 12 -- harnesses/token-trade-coverage/README.md | 98 --------- .../cmd/scanner/bitquery.go | 191 ------------------ .../token-trade-coverage/cmd/scanner/codex.go | 155 -------------- .../cmd/scanner/config.go | 169 ---------------- .../token-trade-coverage/cmd/scanner/main.go | 185 ----------------- .../cmd/scanner/metrics.go | 85 -------- .../cmd/scanner/mobula.go | 156 -------------- harnesses/token-trade-coverage/go.mod | 17 -- harnesses/token-trade-coverage/go.sum | 32 --- 13 files changed, 1274 deletions(-) delete mode 100644 benchmarks/token-trade-coverage.yml delete mode 100644 harnesses/token-trade-coverage/.env.example delete mode 100644 harnesses/token-trade-coverage/.gitignore delete mode 100644 harnesses/token-trade-coverage/Dockerfile delete mode 100644 harnesses/token-trade-coverage/README.md delete mode 100644 harnesses/token-trade-coverage/cmd/scanner/bitquery.go delete mode 100644 harnesses/token-trade-coverage/cmd/scanner/codex.go delete mode 100644 harnesses/token-trade-coverage/cmd/scanner/config.go delete mode 100644 harnesses/token-trade-coverage/cmd/scanner/main.go delete mode 100644 harnesses/token-trade-coverage/cmd/scanner/metrics.go delete mode 100644 harnesses/token-trade-coverage/cmd/scanner/mobula.go delete mode 100644 harnesses/token-trade-coverage/go.mod delete mode 100644 harnesses/token-trade-coverage/go.sum diff --git a/benchmarks/token-trade-coverage.yml b/benchmarks/token-trade-coverage.yml deleted file mode 100644 index 063d2501..00000000 --- a/benchmarks/token-trade-coverage.yml +++ /dev/null @@ -1,131 +0,0 @@ -# OpenChainBench. Bench № 090 - -slug: token-trade-coverage -number: "090" -title: Most complete onchain trade data API, live coverage across Solana, EVM and Stellar -seo_title: "Best token trade data API 2026 (Mobula vs Bitquery vs Codex)" -seo_description: "Which onchain data API returns the most complete trade history for a newly launched token? Live coverage % across Mobula, Bitquery and Codex on Solana, Ethereum, BSC, Base and Stellar." -subtitle: For each reference token per chain, we fetch every trade in a fixed measurement window from each provider and compare against the union baseline. Higher percent means the provider returned more of the true trade set, with fewer missed DEXs and fewer dropped fills. -category: Aggregators -status: live -metric: Trade capture rate -unit: pct -higher_is_better: true - -seo_intro: | - This benchmark measures how much of a token's real trade history each - onchain data API actually returns. For a fixed set of reference tokens - per chain (Solana, Ethereum, BSC, Base, Stellar), the harness fetches - every trade in the same measurement window from Mobula, Bitquery, - Codex, then computes each provider's capture rate against - the union baseline (max count across the three). A rate of 100 means - the provider returned as many trades as the best-covering provider on - that token. A rate of 20 means the provider missed 80 percent of the - swaps that did happen. The comparison is chain-scoped because - coverage is not uniform: a provider strong on Solana DEX indexing can - be weak on niche BSC pools, and no provider covers every chain - equally (Stellar coverage in particular is uneven since only two of - the three measured providers index it at all today). - -abstract: | - Coverage is measured on a fixed set of reference tokens per chain, - querying each provider's public trade endpoint for the same time - window. We take the union of all trades returned across all providers - as the baseline (denominator) and each provider's returned count as - the numerator. Reference tokens are chosen for meaningful trade - volume in the measurement window so a coverage gap actually shows. - Providers use their official public endpoints. Mobula through - /api/2/trades/filters, Bitquery through streaming.bitquery.io - GraphQL, Codex through graph.codex.io GraphQL. - Stellar is measured only for providers that officially list a - Stellar trades endpoint; providers that do not index Stellar at all - are omitted from that chain's row rather than counted as zero. - -methodology: - - "Reference tokens: a fixed set per chain, chosen for enough trade volume in the measurement window that a coverage gap is visible above sampling noise." - - "Measurement window: 60 minutes rolling. The window is the same for every provider in a given cycle so counts are directly comparable." - - "Mobula: `GET https://api.mobula.io/api/2/trades/filters?from={ts_ms}&to={ts_ms}` with `Authorization: `. Cursor pagination, limit=5000." - - "Bitquery: `POST https://streaming.bitquery.io/graphql` with `X-API-KEY`. GraphQL query on `DEXTradeByTokens` with `Block.Time.since` / `till` on the realtime dataset. Hard limit 10000 per call." - - "Codex: `POST https://graph.codex.io/graphql` with `Authorization: `. GraphQL query on `getTokenEvents` with `timestamp` bounds. Cursor pagination." - - "Stellar coverage measured only for providers that ship an official Stellar trades endpoint. Providers with no Stellar support are excluded from the Stellar row rather than counted as zero (that would penalise all three providers unfairly against a chain none of them promised)." - - "Capture rate: `provider_count / max(provider_count across all providers on that (chain, token)) * 100`. Union baseline is the largest observed count, not a synthetic ground truth." - - "Cadence: every 30 minutes per (provider, chain, token). One measurement is short (a handful of API calls per token) so the full cycle finishes well inside the 30-minute window." - - "Failure handling: HTTP timeouts and 5xx count as zero trades for that cycle. Persistent 5xx (three cycles in a row) surfaces as `unresponsive` on the leaderboard, not silent zeros." - -findings: - - "{{best_name}} currently leads at {{best_p50}} average trade capture across {{count}} measured providers. The union baseline updates every cycle: as one provider extends coverage the ceiling moves and the others' relative capture rates shift." - - "{{name:mobula}} averages {{p50:mobula}}. Mobula's advantage on this bench is DEX breadth. Coverage of pump.fun, meme AMMs and small aggregator DEXs is what drives the number up on new-launch Solana tokens where competitors often index only Raydium or Meteora." - - "{{name:bitquery}} averages {{p50:bitquery}}. Bitquery ships strong Solana and EVM coverage through its `DEXTradeByTokens` GraphQL, but the 10000-row hard cap on a single query truncates high-volume tokens in the first hour after launch; the harness paginates when possible, but not every trade type is paginable." - - "{{name:codex}} averages {{p50:codex}}. Codex (formerly Defined) prioritises depth on EVM and Solana DEX event indexing, which shows here as strong per-chain coverage on the chains they index and no coverage at all on Stellar." - - "Stellar coverage is uneven: only providers that officially list a Stellar trades endpoint appear on that chain's row. This is the honest way to represent the current state of the market. Mobula added Stellar recently, others have not shipped it yet." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/token-trade-coverage - -prometheus: - window: 24h - expected_freshness_seconds: 3600 - -faq: - - q: "Which crypto data API returns the most complete trade history for a token?" - a: "{{best_name}} currently leads at {{best_p50}} average trade capture across {{count}} measured providers. This is measured per (chain, token) and averaged, so the leaderboard reflects real coverage across a mixed sample rather than a single lucky token." - - q: "What does 'capture rate' actually mean here?" - a: "For each reference token on each chain, the harness fetches trades from every provider over the same time window. The largest count across providers is the baseline (union of what's discoverable through public APIs). Each provider's rate is its own count divided by that baseline, expressed as a percent. A rate of 100 on a token means the provider returned as many trades as the best-covering provider. A rate of 40 means it missed 60 percent of the swaps the best-covering provider found." - - q: "Why not use a canonical onchain ground truth from RPC nodes?" - a: "In principle you could walk the block explorer and reconstruct every swap. In practice the ground truth depends on which DEX programs, protocols and vault contracts you include, and that decision is itself opinionated. The union-of-providers baseline is a defensible proxy that mirrors what a trader integrating a public API can actually see. It undercounts the truth uniformly across providers, so relative ranking is preserved even if absolute completeness is not 100." - - q: "Which chains are covered?" - a: "Solana, Ethereum, BSC, Base and Stellar. Every provider is measured on each chain it officially supports. Stellar is included because Mobula added Stellar trades to its API recently; other providers that do not ship a Stellar endpoint are excluded from the Stellar row rather than counted as zero." - -dimensions: - chain: - - value: all - label: All chains - - value: solana - label: Solana - - value: ethereum - label: Ethereum - - value: bsc - label: BNB Chain - - value: base - label: Base - - value: stellar - label: Stellar - -providers: - - slug: mobula - name: Mobula - tag: /api/2/trades/filters - formula: "Trades returned by Mobula /api/2/trades/filters divided by union-across-providers count on the same (chain, token) in a rolling 60 min window." - queries: - p50: avg_over_time(ocb_token_trade_capture_pct{provider="mobula"}[24h]) - p90: quantile_over_time(0.90, ocb_token_trade_capture_pct{provider="mobula"}[24h]) - p99: quantile_over_time(0.99, ocb_token_trade_capture_pct{provider="mobula"}[24h]) - mean: avg_over_time(ocb_token_trade_capture_pct{provider="mobula"}[24h]) - success: clamp_max(avg_over_time(ocb_token_trade_probe_ok{provider="mobula"}[24h]), 1) - sample_size: count_over_time(ocb_token_trade_capture_pct{provider="mobula"}[24h]) - series: ocb_token_trade_capture_pct{provider="mobula"} - - - slug: bitquery - name: Bitquery - tag: streaming.bitquery.io GraphQL - formula: "Trades returned by Bitquery DEXTradeByTokens (realtime dataset) divided by union-across-providers count on the same (chain, token) in a rolling 60 min window." - queries: - p50: avg_over_time(ocb_token_trade_capture_pct{provider="bitquery"}[24h]) - p90: quantile_over_time(0.90, ocb_token_trade_capture_pct{provider="bitquery"}[24h]) - p99: quantile_over_time(0.99, ocb_token_trade_capture_pct{provider="bitquery"}[24h]) - mean: avg_over_time(ocb_token_trade_capture_pct{provider="bitquery"}[24h]) - success: clamp_max(avg_over_time(ocb_token_trade_probe_ok{provider="bitquery"}[24h]), 1) - sample_size: count_over_time(ocb_token_trade_capture_pct{provider="bitquery"}[24h]) - series: ocb_token_trade_capture_pct{provider="bitquery"} - - - slug: codex - name: Codex - tag: graph.codex.io GraphQL - formula: "Trades returned by Codex getTokenEvents divided by union-across-providers on the same (chain, token) in a rolling 60 min window. Excluded from Stellar row." - queries: - p50: avg_over_time(ocb_token_trade_capture_pct{provider="codex"}[24h]) - p90: quantile_over_time(0.90, ocb_token_trade_capture_pct{provider="codex"}[24h]) - p99: quantile_over_time(0.99, ocb_token_trade_capture_pct{provider="codex"}[24h]) - mean: avg_over_time(ocb_token_trade_capture_pct{provider="codex"}[24h]) - success: clamp_max(avg_over_time(ocb_token_trade_probe_ok{provider="codex"}[24h]), 1) - sample_size: count_over_time(ocb_token_trade_capture_pct{provider="codex"}[24h]) - series: ocb_token_trade_capture_pct{provider="codex"} diff --git a/harnesses/token-trade-coverage/.env.example b/harnesses/token-trade-coverage/.env.example deleted file mode 100644 index a613cf9a..00000000 --- a/harnesses/token-trade-coverage/.env.example +++ /dev/null @@ -1,41 +0,0 @@ -# token-trade-coverage harness. Fill in and rename to `.env`. - -# Provider keys (harness tolerates missing keys per-provider; missing -# key sets probe_ok=0 for that provider only, others keep going). -MOBULA_API_KEY= -BITQUERY_API_KEY= -CODEX_API_KEY= - -# ─── Cadence ────────────────────────────────────────────────────────── -# Base sweep cadence. 3600 = 1 hour. Every SWEEP_SEC the harness iterates -# every (provider, chain, token) tuple, respecting the per-provider -# EveryN sub-sampling below. -SWEEP_SEC=3600 - -# ─── Per-provider sub-sampling (respects free-tier quotas) ──────────── -# A provider runs only when `iteration % EveryN == 0`. Iteration is the -# 0-indexed sweep counter (boot = 0, first tick = 1, ...). -# -# Default free-tier budget math (at SWEEP_SEC=3600, 8 EVM+Solana tokens, -# 2 Stellar tokens Mobula-only): -# Mobula MOBULA_EVERY_N=1 → 240 calls / day = free (own API) -# Bitquery BITQUERY_EVERY_N=6 → 8 tokens × 4 sweeps/day × 30 = 960 -# calls / month at ~1 pt each = ~960 pts (Free plan cap 1000) -# Codex CODEX_EVERY_N=1 → 192 calls / day = fits Codex rate limits -# -# On a paid Bitquery plan (Developer $99/mo = 500k pts), set BITQUERY_EVERY_N=1. -MOBULA_EVERY_N=1 -BITQUERY_EVERY_N=6 -CODEX_EVERY_N=1 - -# ─── Query caps (prevent runaway pagination on heavy tokens) ────────── -# Mobula: cursor pages, 5000 rows each → MAX_PAGES=20 caps at 100k trades / token / sweep. -# Bitquery: single-shot query, no cursor → MAX_ROWS is the `limit` in the GraphQL. -# Codex: cursor pages, 200 rows each → MAX_PAGES=10 caps at 2000 events / token / sweep. -MOBULA_MAX_PAGES=20 -BITQUERY_MAX_ROWS=10000 -CODEX_MAX_PAGES=10 - -# ─── Runtime knobs ──────────────────────────────────────────────────── -METRICS_PORT=2112 -HTTP_TIMEOUT_SEC=30 diff --git a/harnesses/token-trade-coverage/.gitignore b/harnesses/token-trade-coverage/.gitignore deleted file mode 100644 index a0113a2f..00000000 --- a/harnesses/token-trade-coverage/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.env -/scanner diff --git a/harnesses/token-trade-coverage/Dockerfile b/harnesses/token-trade-coverage/Dockerfile deleted file mode 100644 index 0b6e7282..00000000 --- a/harnesses/token-trade-coverage/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.23-alpine AS build -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/scanner ./cmd/scanner - -FROM alpine:3.20 -RUN apk add --no-cache ca-certificates && update-ca-certificates -COPY --from=build /out/scanner /usr/local/bin/scanner -EXPOSE 2112 -ENTRYPOINT ["/usr/local/bin/scanner"] diff --git a/harnesses/token-trade-coverage/README.md b/harnesses/token-trade-coverage/README.md deleted file mode 100644 index d32dc009..00000000 --- a/harnesses/token-trade-coverage/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Harness · token-trade-coverage - -> Source for bench № 090 · Most complete onchain trade data API. Measures, for each reference token per chain, how many trades each provider (Mobula, Bitquery, Codex, Moralis) returns in a fixed 60-minute window, then publishes the capture rate vs the union baseline. - -## What ships in this directory - -- `cmd/scanner/main.go` — measurement loop. Every `SWEEP_SEC` (default 1800 = 30 min) it iterates every (provider, chain, token) tuple, fetches trades in the same 60-minute window, computes the union baseline as `max(counts across providers)` and emits per-provider capture rate to Prometheus. -- `cmd/scanner/mobula.go`, `bitquery.go`, `codex.go`, `moralis.go` — one file per provider. Each exposes a single `fetchTrades(ctx, chain, tokenAddress, windowStart, windowEnd) (int, error)` function that returns the number of distinct trades. Not the full trade objects — we only need the count for capture-rate computation, so the harness never materializes hundreds of MB of trade JSON in memory. -- `cmd/scanner/config.go` — reference token list per chain, env parsing, provider capability matrix (which provider supports which chain). -- `cmd/scanner/metrics.go` — Prom metric definitions (`ocb_token_trade_capture_pct`, `ocb_token_trade_probe_ok`, `ocb_token_trade_query_latency_ms`, `ocb_token_trade_dex_count`). - -## Providers - -| Provider | Endpoint | Auth | Chains | -| --- | --- | --- | --- | -| Mobula | `GET /api/2/trades/filters` | `Authorization: ` | Solana, Ethereum, BSC, Base, Stellar | -| Bitquery | `POST /graphql` (streaming.bitquery.io) | `X-API-KEY` header | Solana, Ethereum, BSC, Base | -| Codex | `POST /graphql` (graph.codex.io) | `Authorization: ` | Solana, Ethereum, BSC, Base | -| Moralis | `GET /token/mainnet/{addr}/swaps` (Solana) or `GET /erc20/{addr}/swaps` (EVM) | `X-API-Key` | Solana, Ethereum, BSC, Base | - -Stellar is measured for Mobula only (the other three do not ship a public Stellar trades endpoint at time of writing). The bench renders Stellar coverage as a per-chain view where non-supporting providers are absent from the row, not counted as zero. - -## Reference tokens - -Chosen for meaningful trade activity in the measurement window so a coverage gap is visible above sampling noise. See `config.go` for the current list. Rotated periodically to avoid a single token going illiquid and dragging every provider's absolute count to zero. - -## Cadence - -`SWEEP_SEC=1800` (30 min). One full sweep does 4 providers × 5 chains × 2 tokens = up to 40 API calls (fewer when a provider does not support a chain). Each call is bounded by `HTTP_TIMEOUT_SEC=30`. A full sweep completes well inside 30 minutes so `SWEEP_SEC` cadence and `avg_over_time(...[24h])` on the spec queries stay honest. - -## Env vars - -Required: - -- `MOBULA_API_KEY` — Mobula API key. Contact mobula.io if you don't have one. -- `BITQUERY_API_KEY` — Bitquery streaming.bitquery.io key. -- `CODEX_API_KEY` — Codex (Defined) API key. NOTE: this bench does NOT use the cookie-based JWT flow from `aggregator-head-lag` because the query volume is high (batch historical, not live subscribe). Fresh dedicated key recommended. -- `MORALIS_API_KEY` — Moralis Web3 Data API key. - -Optional: - -- `SWEEP_SEC` (default 1800) -- `METRICS_PORT` (default 2112) -- `HTTP_TIMEOUT_SEC` (default 30) -- `LOG_LEVEL` (default info; set debug to log every provider call) - -## Metrics produced - -| Metric | Description | -| --- | --- | -| `ocb_token_trade_capture_pct{provider, chain, token}` | Capture rate percent (0-100). `provider_count / max_provider_count * 100`. Union baseline is per (chain, token) per cycle. | -| `ocb_token_trade_absolute_count{provider, chain, token}` | Raw trade count returned by that provider in the measurement window. | -| `ocb_token_trade_query_latency_ms{provider, chain, token}` | Wall-clock latency of the provider call, including pagination. | -| `ocb_token_trade_dex_count{provider, chain, token}` | Distinct DEX venues represented in the returned trade set. Companion metric — coverage breadth vs pure count. | -| `ocb_token_trade_probe_ok{provider, chain, token}` | 1 on successful fetch, 0 on error/timeout. Consumed by the spec's `success` query. | - -The spec at `benchmarks/token-trade-coverage.yml` aggregates these across (chain, token) into a per-provider p50 for the headline leaderboard. - -## Run locally - -```bash -cp .env.example .env -# Fill in the four API keys -go run ./cmd/scanner/ -``` - -`/metrics` at `http://localhost:2112/metrics`. - -## Run in the OCB VPS stack - -Add a service block in `/opt/ocb/docker-compose.yml`: - -```yaml - token-trade-coverage: - build: - context: /opt/ocb/harnesses/token-trade-coverage - dockerfile: Dockerfile - container_name: ocb-token-trade-coverage - restart: unless-stopped - env_file: /run/ocb/.env.token-trade-coverage - expose: ["2112"] - networks: [web] - mem_limit: 512m - cpus: "0.3" -``` - -Prometheus scrape config additions in `/opt/ocb/prometheus.yml`: - -```yaml - - job_name: token-trade-coverage - scrape_interval: 60s - static_configs: - - targets: ["token-trade-coverage:2112"] -``` - -## Reference implementation - -The initial TypeScript reference lives at https://github.com/Flotapponnier/token-trade-benchmark-. This harness is a Go port with Prometheus emission and OCB conventions. Any semantic drift between the two (which trades count, how the window is bounded) is corrected here first; the reference repo is a design document, not a source of truth. diff --git a/harnesses/token-trade-coverage/cmd/scanner/bitquery.go b/harnesses/token-trade-coverage/cmd/scanner/bitquery.go deleted file mode 100644 index 2d2005cf..00000000 --- a/harnesses/token-trade-coverage/cmd/scanner/bitquery.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -const bitqueryURL = "https://streaming.bitquery.io/graphql" - -// bitqueryEVMResp / bitquerySolanaResp keep the response tight: only -// what we need to count trades and derive DEX venues. -type bitqueryEVMResp struct { - Data struct { - EVM struct { - DEXTrades []struct { - Transaction struct { - Hash string `json:"Hash"` - } `json:"Transaction"` - Trade struct { - Dex struct { - ProtocolName string `json:"ProtocolName"` - } `json:"Dex"` - } `json:"Trade"` - } `json:"DEXTrades"` - } `json:"EVM"` - } `json:"data"` -} - -type bitquerySolanaResp struct { - Data struct { - Solana struct { - DEXTradeByTokens []struct { - Transaction struct { - Signature string `json:"Signature"` - } `json:"Transaction"` - Trade struct { - Dex struct { - ProtocolName string `json:"ProtocolName"` - } `json:"Dex"` - } `json:"Trade"` - } `json:"DEXTradeByTokens"` - } `json:"Solana"` - } `json:"data"` -} - -// fetchBitquery returns (trade count, distinct DEXs, error). Bitquery -// caps a single query at 10000 rows and has no cursor pagination on -// DEXTradeByTokens; on tokens whose true trade count exceeds 10k the -// bench under-reports Bitquery vs a paginated provider. The findings -// section of the spec calls this out honestly rather than pretending -// Bitquery is worse than it is. -func fetchBitquery( - ctx context.Context, - client *http.Client, - apiKey string, - tok Token, - windowStart, windowEnd int64, - maxRows int, -) (int, int, error) { - if apiKey == "" { - return 0, 0, fmt.Errorf("BITQUERY_API_KEY not set") - } - if maxRows <= 0 { - maxRows = 10000 - } - - sinceISO := time.Unix(windowStart/1000, 0).UTC().Format(time.RFC3339) - tillISO := time.Unix(windowEnd/1000, 0).UTC().Format(time.RFC3339) - - var query string - if tok.Chain == "solana" { - query = solanaTradesGQL(tok.Address, sinceISO, tillISO, maxRows) - } else { - network := bitqueryEVMNetwork(tok.Chain) - if network == "" { - return 0, 0, fmt.Errorf("bitquery: unsupported chain %s", tok.Chain) - } - query = evmTradesGQL(network, tok.Address, sinceISO, tillISO, maxRows) - } - - body, _ := json.Marshal(map[string]string{"query": query}) - req, err := http.NewRequestWithContext(ctx, "POST", bitqueryURL, bytes.NewReader(body)) - if err != nil { - return 0, 0, err - } - // Bitquery's new OAuth-style keys (ory_at_*) require Bearer auth on - // streaming.bitquery.io/graphql. The legacy X-API-KEY header on that - // endpoint returns HTTP 402 "No active billing period" even when the - // account has an active free plan — the migration was silent and the - // docs still show the old header on some pages. - req.Header.Set("Authorization", "Bearer "+apiKey) - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - return 0, 0, err - } - respBody, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return 0, 0, err - } - if resp.StatusCode >= 300 { - return 0, 0, fmt.Errorf("bitquery http %d: %s", resp.StatusCode, truncate(string(respBody), 200)) - } - - dexSet := map[string]struct{}{} - hashSet := map[string]struct{}{} - - if tok.Chain == "solana" { - var r bitquerySolanaResp - if err := json.Unmarshal(respBody, &r); err != nil { - return 0, 0, fmt.Errorf("bitquery parse: %w", err) - } - for _, t := range r.Data.Solana.DEXTradeByTokens { - if _, seen := hashSet[t.Transaction.Signature]; seen { - continue - } - hashSet[t.Transaction.Signature] = struct{}{} - if t.Trade.Dex.ProtocolName != "" { - dexSet[t.Trade.Dex.ProtocolName] = struct{}{} - } - } - } else { - var r bitqueryEVMResp - if err := json.Unmarshal(respBody, &r); err != nil { - return 0, 0, fmt.Errorf("bitquery parse: %w", err) - } - for _, t := range r.Data.EVM.DEXTrades { - if _, seen := hashSet[t.Transaction.Hash]; seen { - continue - } - hashSet[t.Transaction.Hash] = struct{}{} - if t.Trade.Dex.ProtocolName != "" { - dexSet[t.Trade.Dex.ProtocolName] = struct{}{} - } - } - } - return len(hashSet), len(dexSet), nil -} - -func bitqueryEVMNetwork(chain string) string { - switch chain { - case "ethereum": - return "eth" - case "bsc": - return "bsc" - case "base": - return "base" - } - return "" -} - -func solanaTradesGQL(mintAddress, since, till string, maxRows int) string { - return fmt.Sprintf(`{ - Solana(dataset: realtime) { - DEXTradeByTokens( - limit: {count: %d} - where: { - Block: {Time: {since: "%s", till: "%s"}} - Trade: {Currency: {MintAddress: {is: "%s"}}} - } - ) { - Transaction { Signature } - Trade { Dex { ProtocolName } } - } - } -}`, maxRows, since, till, mintAddress) -} - -func evmTradesGQL(network, tokenAddress, since, till string, maxRows int) string { - return fmt.Sprintf(`{ - EVM(dataset: realtime, network: %s) { - DEXTrades( - limit: {count: %d} - where: { - Block: {Time: {since: "%s", till: "%s"}} - Trade: {Buy: {Currency: {SmartContract: {is: "%s"}}}} - } - ) { - Transaction { Hash } - Trade { Dex { ProtocolName } } - } - } -}`, network, maxRows, since, till, tokenAddress) -} diff --git a/harnesses/token-trade-coverage/cmd/scanner/codex.go b/harnesses/token-trade-coverage/cmd/scanner/codex.go deleted file mode 100644 index 9b19b224..00000000 --- a/harnesses/token-trade-coverage/cmd/scanner/codex.go +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" -) - -const codexURL = "https://graph.codex.io/graphql" - -// codexResp: minimal envelope. Codex's `getTokenEvents` is cursor-paginated -// via the `cursor` string returned in each response. -// -// Fields kept minimal on purpose. `exchangeAddress` was dropped from -// the query because it's not a valid field on Codex's `Event` type -// (GraphQL 400: `Cannot query field exchangeAddress on type Event`). -// Codex's DEX identity lives one hop away on the `Pair.exchange` type, -// which would require joining `Pair` records per event and inflates -// the query volume for a companion metric that we already publish -// honestly (0) for a provider that doesn't expose it inline. -type codexResp struct { - Data struct { - GetTokenEvents struct { - Items []struct { - TransactionHash string `json:"transactionHash"` - EventDisplayType string `json:"eventDisplayType"` - } `json:"items"` - Cursor string `json:"cursor"` - } `json:"getTokenEvents"` - } `json:"data"` - Errors []struct { - Message string `json:"message"` - } `json:"errors"` -} - -// codexNetworkID maps OCB chain slug to Codex's numeric networkId. -// Same table as in the reference TS impl. -func codexNetworkID(chain string) int { - switch chain { - case "solana": - return 1399811149 - case "ethereum": - return 1 - case "bsc": - return 56 - case "base": - return 8453 - } - return 0 -} - -// fetchCodex returns (trade count, distinct exchanges, error). Uses -// standard API-key auth via `Authorization` header — the cookie-based -// JWT flow that lives in aggregator-head-lag is not used here because -// query volume is batch-historical, not live-subscribe. -func fetchCodex( - ctx context.Context, - client *http.Client, - apiKey string, - tok Token, - windowStart, windowEnd int64, - maxPages int, -) (int, int, error) { - if apiKey == "" { - return 0, 0, fmt.Errorf("CODEX_API_KEY not set") - } - if maxPages <= 0 { - maxPages = 10 - } - networkID := codexNetworkID(tok.Chain) - if networkID == 0 { - return 0, 0, fmt.Errorf("codex: unsupported chain %s", tok.Chain) - } - - fromSec := windowStart / 1000 - toSec := windowEnd / 1000 - - var ( - total int - cursor string - exchSet = map[string]struct{}{} - hashSet = map[string]struct{}{} - ) - - for page := 0; page < maxPages; page++ { - var cursorClause string - if cursor != "" { - cursorClause = fmt.Sprintf(`cursor: %q`, cursor) - } - query := fmt.Sprintf(` -query GetEvents { - getTokenEvents( - query: { - address: "%s" - networkId: %d - timestamp: {from: %d, to: %d} - eventType: Swap - } - limit: 200 - %s - ) { - items { - transactionHash - eventDisplayType - } - cursor - } -}`, tok.Address, networkID, fromSec, toSec, cursorClause) - - body, _ := json.Marshal(map[string]string{"query": query}) - req, err := http.NewRequestWithContext(ctx, "POST", codexURL, bytes.NewReader(body)) - if err != nil { - return total, len(exchSet), err - } - req.Header.Set("Authorization", apiKey) - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - return total, len(exchSet), err - } - respBody, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return total, len(exchSet), err - } - if resp.StatusCode >= 300 { - return total, len(exchSet), fmt.Errorf("codex http %d: %s", resp.StatusCode, truncate(string(respBody), 200)) - } - var r codexResp - if err := json.Unmarshal(respBody, &r); err != nil { - return total, len(exchSet), fmt.Errorf("codex parse: %w", err) - } - if len(r.Errors) > 0 { - return total, len(exchSet), fmt.Errorf("codex graphql: %s", r.Errors[0].Message) - } - for _, it := range r.Data.GetTokenEvents.Items { - if _, seen := hashSet[it.TransactionHash]; seen { - continue - } - hashSet[it.TransactionHash] = struct{}{} - total++ - // Codex Event doesn't expose DEX inline; leave dexCount=0 - // rather than fabricating a synthetic venue. - } - if r.Data.GetTokenEvents.Cursor == "" || len(r.Data.GetTokenEvents.Items) == 0 { - break - } - cursor = r.Data.GetTokenEvents.Cursor - } - return total, len(exchSet), nil -} diff --git a/harnesses/token-trade-coverage/cmd/scanner/config.go b/harnesses/token-trade-coverage/cmd/scanner/config.go deleted file mode 100644 index 68717aca..00000000 --- a/harnesses/token-trade-coverage/cmd/scanner/config.go +++ /dev/null @@ -1,169 +0,0 @@ -package main - -import ( - "os" - "strconv" - "time" -) - -// Token is a reference token watched for coverage measurement. -type Token struct { - Chain string // "solana", "ethereum", "bsc", "base", "stellar" - Address string // native address on that chain - Symbol string // short label for logs -} - -// ProviderCapability declares which chains a provider is measured on. -// A provider is EXCLUDED from a chain's row if the chain is not listed -// here; that keeps the leaderboard honest instead of counting a -// non-integration as a zero-score defeat. -type ProviderCapability map[string]bool - -// Config is populated once at startup and read-only afterwards. -type Config struct { - SweepSec int - MetricsPort string - HTTPTimeoutSec int - MobulaKey string - BitqueryKey string - CodexKey string - Tokens []Token - Capabilities map[string]ProviderCapability // provider -> {chain -> supported} - MeasurementWinMs int64 // rolling window per measurement - - // Per-provider sub-sampling (respects free-tier quotas). - // A provider only runs when `iteration % everyN == 0`. Default 1 - // means every sweep. Higher = sparser samples but preserves free- - // tier point budget on providers with tight limits (Bitquery Free - // gives 1000 points / month, so on the default 60-min cadence with - // EveryN=6 we spend ≤ 960 points / month across 8 supported tokens). - MobulaEveryN int - BitqueryEveryN int - CodexEveryN int - - // Page caps: bound the max pagination pages per provider per token - // per sweep. Prevents runaway cursor loops on a single very-heavy - // token from burning a whole sweep's quota. - MobulaMaxPages int - BitqueryMaxRows int // Bitquery has no cursor — this is the per-query row cap - CodexMaxPages int -} - -// LoadConfig reads env, defaults + hardcoded reference token list. -// Reference tokens are chosen for meaningful trade activity so a -// coverage gap becomes visible; rotate them here when a listed token -// goes illiquid. -func LoadConfig() *Config { - return &Config{ - // Base cadence: 60 min. Doubled from the initial 30-min value - // after quota math on Bitquery's 1000-pt free plan (see - // BitqueryEveryN below). 24 sweeps / day gives >= 12 samples per - // (chain, token) per day for providers that run every sweep — - // plenty for a stable p50 over 24h. - SweepSec: envInt("SWEEP_SEC", 3600), - MetricsPort: envStr("METRICS_PORT", "2112"), - HTTPTimeoutSec: envInt("HTTP_TIMEOUT_SEC", 30), - MeasurementWinMs: int64(60*60) * 1000, // 60 min rolling window - MobulaKey: os.Getenv("MOBULA_API_KEY"), - BitqueryKey: os.Getenv("BITQUERY_API_KEY"), - CodexKey: os.Getenv("CODEX_API_KEY"), - - // Mobula: unlimited (own infra). Run every sweep. - MobulaEveryN: envInt("MOBULA_EVERY_N", 1), - // Bitquery Free: 1000 pts / month. At ~1-2 pts per - // DEXTradeByTokens call and 8 supported tokens per sweep, - // running every 6th sweep = 4 sweeps / day × 8 tokens × 30 - // days = 960 calls / month, well inside the free budget. - // Bump to 1 if on the Developer plan ($99/mo, 500k pts). - BitqueryEveryN: envInt("BITQUERY_EVERY_N", 6), - // Codex Free: generous rate-based limits (no monthly point - // cap on the current tier). Run every sweep. - CodexEveryN: envInt("CODEX_EVERY_N", 1), - - // Page caps. A high-volume token that fires cursor-paginated - // requests indefinitely can drain a monthly budget in one - // sweep. These bound the worst case per token per sweep. - MobulaMaxPages: envInt("MOBULA_MAX_PAGES", 20), - BitqueryMaxRows: envInt("BITQUERY_MAX_ROWS", 10000), - CodexMaxPages: envInt("CODEX_MAX_PAGES", 10), - Tokens: []Token{ - // Reference tokens selected 2026-07-23 via a Mobula - // `/api/2/trades/filters` sweep over the last hour, picking - // mid-liquidity actives (not pure blue chips, so provider - // coverage differences show; not pump.fun day-olds, so the - // bench doesn't die if a specific token's activity dries up - // tomorrow). All 10 verified > 50 trades / 1 h at pick time. - // Rotate here when a listed token's volume drops off. - // - // Solana. BONK and WIF are both well past the pump.fun window - // and trade across every Solana DEX indexed by the three - // providers, which is what surfaces real coverage differences. - {Chain: "solana", Address: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", Symbol: "BONK"}, - {Chain: "solana", Address: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", Symbol: "WIF"}, - // Ethereum. PEPE covers the top of the meme distribution; - // MOG sits mid-liquidity and often reveals per-DEX gaps that - // PEPE's wall-to-wall coverage masks. - {Chain: "ethereum", Address: "0x6982508145454Ce325dDbE47a25d4ec3d2311933", Symbol: "PEPE"}, - {Chain: "ethereum", Address: "0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a", Symbol: "MOG"}, - // BSC. CAKE is the anchor (own PancakeSwap listings); FLOKI - // trades across PancakeSwap + long-tail BEP-20 AMMs so the - // per-DEX split shows. - {Chain: "bsc", Address: "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82", Symbol: "CAKE"}, - {Chain: "bsc", Address: "0xfb5B838b6cfEEdC2873aB27866079AC55363D37E", Symbol: "FLOKI"}, - // Base. BRETT and DEGEN — top two active memes on Aerodrome - // + Uniswap v3, wide enough coverage on Base that they're - // stable picks across weeks. - {Chain: "base", Address: "0x532f27101965dd16442E59d40670FaF5eBB142E4", Symbol: "BRETT"}, - {Chain: "base", Address: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed", Symbol: "DEGEN"}, - // Stellar. yXLM (wrapped XLM) and SHX (Stronghold) are both - // high-activity Stellar-native assets. Address format is - // Stellar's canonical `:` string — Mobula - // accepts that directly as `tokenAddress`. - {Chain: "stellar", Address: "yXLM:GARDNV3Q7YGT4AKSDF25LT32YSCCW4EV22Y2TV3I2PU2MMXJTEDL5T55", Symbol: "yXLM"}, - {Chain: "stellar", Address: "SHX:GDSTRSHXHGJ7ZIVRBXEYE5Q74XUVCUSEKEBR7UCHEUUEK72N7I7KJ6JH", Symbol: "SHX"}, - }, - Capabilities: map[string]ProviderCapability{ - "mobula": { - "solana": true, "ethereum": true, "bsc": true, "base": true, "stellar": true, - }, - "bitquery": { - "solana": true, "ethereum": true, "bsc": true, "base": true, - }, - "codex": { - "solana": true, "ethereum": true, "bsc": true, "base": true, - }, - }, - } -} - -func (c *Config) HTTPTimeout() time.Duration { - return time.Duration(c.HTTPTimeoutSec) * time.Second -} - -// Supports reports whether the (provider, chain) pair is measured. -func (c *Config) Supports(provider, chain string) bool { - caps, ok := c.Capabilities[provider] - if !ok { - return false - } - return caps[chain] -} - -func envInt(k string, def int) int { - v := os.Getenv(k) - if v == "" { - return def - } - n, err := strconv.Atoi(v) - if err != nil { - return def - } - return n -} - -func envStr(k, def string) string { - if v := os.Getenv(k); v != "" { - return v - } - return def -} diff --git a/harnesses/token-trade-coverage/cmd/scanner/main.go b/harnesses/token-trade-coverage/cmd/scanner/main.go deleted file mode 100644 index 017d4560..00000000 --- a/harnesses/token-trade-coverage/cmd/scanner/main.go +++ /dev/null @@ -1,185 +0,0 @@ -// Materialize the token-trade-coverage bench (№ 090). -// -// Loop: every SWEEP_SEC, iterate every (provider, chain, token) tuple, -// fetch trades in the same rolling 60-minute window, compute the union -// baseline per (chain, token) as max(counts) across providers and -// publish capture rate + companion metrics to Prometheus. -// -// The design keeps memory bounded: providers return counts, not trade -// arrays, so a token with 50k trades in the window costs O(1) here. - -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "sync" - "syscall" - "time" - - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var providers = []string{"mobula", "bitquery", "codex"} - -func main() { - cfg := LoadConfig() - log.Printf("[boot] sweep=%ds providers=%v tokens=%d", cfg.SweepSec, providers, len(cfg.Tokens)) - - // Expose /metrics before the first sweep so Prom starts scraping - // immediately; capture_pct just stays 0 until the loop populates it. - http.Handle("/metrics", promhttp.Handler()) - go func() { - addr := ":" + cfg.MetricsPort - log.Printf("[boot] metrics on %s/metrics", addr) - if err := http.ListenAndServe(addr, nil); err != nil { - log.Fatalf("http: %v", err) - } - }() - - client := &http.Client{Timeout: cfg.HTTPTimeout()} - - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer cancel() - - ticker := time.NewTicker(time.Duration(cfg.SweepSec) * time.Second) - defer ticker.Stop() - - // First sweep runs immediately at boot so Prom sees data within - // SWEEP_SEC + measurement time rather than waiting a full cycle. - iteration := 0 - sweep(ctx, cfg, client, iteration) - for { - select { - case <-ctx.Done(): - log.Printf("[shutdown] signal received") - return - case <-ticker.C: - iteration++ - sweep(ctx, cfg, client, iteration) - } - } -} - -// providerEnabled reports whether the provider should run on this -// iteration given its sub-sampling cadence (see Config.*EveryN). -func providerEnabled(cfg *Config, provider string, iteration int) bool { - var everyN int - switch provider { - case "mobula": - everyN = cfg.MobulaEveryN - case "bitquery": - everyN = cfg.BitqueryEveryN - case "codex": - everyN = cfg.CodexEveryN - default: - return false - } - if everyN <= 1 { - return true - } - return iteration%everyN == 0 -} - -// sweep runs one full measurement cycle. For each token, we fan out -// across every supported provider in parallel, wait for all to finish -// (or fail), compute the union baseline as max(counts) and emit. -// -// `iteration` is the 0-indexed sweep counter — used for per-provider -// sub-sampling (see providerEnabled). Skipped providers keep their -// previous capture_pct value in Prom, which `avg_over_time([24h])` -// then averages across the sparse sample. -func sweep(ctx context.Context, cfg *Config, client *http.Client, iteration int) { - sweepStart := time.Now() - windowEnd := sweepStart.UnixMilli() - windowStart := windowEnd - cfg.MeasurementWinMs - - activeProviders := make([]string, 0, len(providers)) - for _, p := range providers { - if providerEnabled(cfg, p, iteration) { - activeProviders = append(activeProviders, p) - } - } - log.Printf("[sweep] iter=%d active=%v", iteration, activeProviders) - - for _, tok := range cfg.Tokens { - if tok.Address == "" { - // Placeholder row (Stellar addresses TBD). Skip silently - // so the container doesn't spam warnings until they land. - continue - } - results := make([]Result, 0, len(activeProviders)) - var mu sync.Mutex - var wg sync.WaitGroup - - for _, p := range activeProviders { - if !cfg.Supports(p, tok.Chain) { - continue - } - wg.Add(1) - go func(p string) { - defer wg.Done() - start := time.Now() - count, dex, err := fetchOne(ctx, client, cfg, p, tok, windowStart, windowEnd) - latMs := float64(time.Since(start).Milliseconds()) - r := Result{ - Provider: p, - Chain: tok.Chain, - Token: tok.Symbol, - Count: count, - DexCount: dex, - LatencyMs: latMs, - OK: err == nil, - } - if err != nil { - log.Printf("[%s][%s/%s] err: %v", p, tok.Chain, tok.Symbol, err) - } else { - log.Printf("[%s][%s/%s] count=%d dex=%d %.0fms", p, tok.Chain, tok.Symbol, count, dex, latMs) - } - mu.Lock() - results = append(results, r) - mu.Unlock() - }(p) - } - wg.Wait() - - // Union baseline: the largest count observed across providers - // that succeeded. If every provider failed the baseline is 0 - // and capture rate stays 0 across the board (spec `success` - // query then flags the (chain, token) as unresponsive). - unionMax := 0 - for _, r := range results { - if r.OK && r.Count > unionMax { - unionMax = r.Count - } - } - emitCycle(results, unionMax) - } - log.Printf("[sweep] done in %.1fs", time.Since(sweepStart).Seconds()) -} - -// fetchOne dispatches to the provider-specific fetcher, threading each -// provider's page/row cap so a runaway pagination loop can never drain -// a monthly quota in a single sweep. -func fetchOne( - ctx context.Context, - client *http.Client, - cfg *Config, - provider string, - tok Token, - windowStart, windowEnd int64, -) (int, int, error) { - switch provider { - case "mobula": - return fetchMobula(ctx, client, cfg.MobulaKey, tok, windowStart, windowEnd, cfg.MobulaMaxPages) - case "bitquery": - return fetchBitquery(ctx, client, cfg.BitqueryKey, tok, windowStart, windowEnd, cfg.BitqueryMaxRows) - case "codex": - return fetchCodex(ctx, client, cfg.CodexKey, tok, windowStart, windowEnd, cfg.CodexMaxPages) - } - return 0, 0, fmt.Errorf("unknown provider %s", provider) -} diff --git a/harnesses/token-trade-coverage/cmd/scanner/metrics.go b/harnesses/token-trade-coverage/cmd/scanner/metrics.go deleted file mode 100644 index 3481c072..00000000 --- a/harnesses/token-trade-coverage/cmd/scanner/metrics.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -// Metric surface. Labels are (provider, chain, token). Kept exactly -// three labels so PromQL aggregation stays simple and the spec's -// `series` query at the bench level can group by any one of them. -var ( - capturePct = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ocb_token_trade_capture_pct", - Help: "Percent of the union-baseline trade count returned by this provider for (chain, token) in the last measurement window.", - }, []string{"provider", "chain", "token"}) - - absoluteCount = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ocb_token_trade_absolute_count", - Help: "Raw trade count returned by this provider for (chain, token) in the last measurement window.", - }, []string{"provider", "chain", "token"}) - - queryLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ocb_token_trade_query_latency_ms", - Help: "Wall-clock latency of the provider's trade fetch call including pagination, in milliseconds.", - }, []string{"provider", "chain", "token"}) - - dexCount = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ocb_token_trade_dex_count", - Help: "Distinct DEX venues represented in the trade set returned by this provider for (chain, token).", - }, []string{"provider", "chain", "token"}) - - probeOK = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ocb_token_trade_probe_ok", - Help: "1 on successful fetch, 0 on error or timeout. Consumed by the bench spec's `success` query.", - }, []string{"provider", "chain", "token"}) - - // Cumulative API-call counter for quota observability. Counted at - // the fetchOne granularity — one increment per (provider, token) - // tuple in a sweep, regardless of how many paginated sub-requests - // fired underneath — because that's what maps 1:1 to the provider's - // monthly point/credit budget as billed. `increase(...[30d])` per - // provider gives the running monthly consumption. - apiCalls = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "ocb_token_trade_api_calls_total", - Help: "Total fetchOne invocations per provider since worker start. Query with increase()[30d] for monthly consumption vs free-tier budget.", - }, []string{"provider"}) -) - -// Result is the per-call outcome of one provider fetch. -type Result struct { - Provider string - Chain string - Token string - Count int - DexCount int - LatencyMs float64 - OK bool -} - -// emitCycle publishes one full cycle's worth of results. Called once -// per (chain, token) after every provider has been queried and the -// union baseline is known. -func emitCycle(results []Result, unionMax int) { - for _, r := range results { - lbl := prometheus.Labels{ - "provider": r.Provider, - "chain": r.Chain, - "token": r.Token, - } - absoluteCount.With(lbl).Set(float64(r.Count)) - queryLatency.With(lbl).Set(r.LatencyMs) - dexCount.With(lbl).Set(float64(r.DexCount)) - if r.OK { - probeOK.With(lbl).Set(1) - } else { - probeOK.With(lbl).Set(0) - } - if unionMax > 0 && r.OK { - capturePct.With(lbl).Set(float64(r.Count) / float64(unionMax) * 100) - } else { - capturePct.With(lbl).Set(0) - } - apiCalls.WithLabelValues(r.Provider).Inc() - } -} diff --git a/harnesses/token-trade-coverage/cmd/scanner/mobula.go b/harnesses/token-trade-coverage/cmd/scanner/mobula.go deleted file mode 100644 index ff32cd68..00000000 --- a/harnesses/token-trade-coverage/cmd/scanner/mobula.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" -) - -const mobulaBase = "https://api.mobula.io/api/2/trades/filters" - -// mobulaResp is the minimal envelope we care about: we only need the -// count (via len(data)) and the DEX per row for coverage-breadth. Full -// trade objects are 30+ fields we intentionally ignore. -// -// Field names track Mobula's `/api/2/trades/filters` response as of -// 2026-07-23. `transactionHash` doubles as the dedup key across -// paginated pages. DEX identity comes from `platform.name` when the -// venue is a mapped protocol (Raydium, Uniswap v3, …); otherwise the -// pool address in `marketAddress` acts as a fallback venue proxy. -type mobulaTrade struct { - TransactionHash string `json:"transactionHash"` - Platform *struct { - Name string `json:"name"` - } `json:"platform"` - MarketAddress string `json:"marketAddress"` -} - -type mobulaResp struct { - Data []mobulaTrade `json:"data"` - Pagination struct { - HasMore bool `json:"hasMore"` - NextCursor string `json:"nextCursor"` - } `json:"pagination"` -} - -// fetchMobula returns (trade count, distinct DEXs, error). Paginates -// via cursor until either exhausted or the safety cap is hit. -func fetchMobula( - ctx context.Context, - client *http.Client, - apiKey string, - tok Token, - windowStart, windowEnd int64, - maxPages int, -) (int, int, error) { - if apiKey == "" { - return 0, 0, fmt.Errorf("MOBULA_API_KEY not set") - } - if maxPages <= 0 { - maxPages = 20 - } - var ( - total int - cursor string - dexSet = map[string]struct{}{} - hashSet = map[string]struct{}{} - ) - for page := 0; page < maxPages; page++ { - u, _ := url.Parse(mobulaBase) - q := u.Query() - q.Set("blockchain", mobulaChainName(tok.Chain)) - q.Set("tokenAddress", tok.Address) - q.Set("from", strconv.FormatInt(windowStart, 10)) - q.Set("to", strconv.FormatInt(windowEnd, 10)) - q.Set("limit", "5000") - q.Set("sortOrder", "asc") - if cursor != "" { - q.Set("cursor", cursor) - } - u.RawQuery = q.Encode() - - req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) - if err != nil { - return total, len(dexSet), err - } - req.Header.Set("Authorization", apiKey) - req.Header.Set("Accept", "application/json") - - resp, err := client.Do(req) - if err != nil { - return total, len(dexSet), err - } - body, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return total, len(dexSet), err - } - if resp.StatusCode >= 300 { - return total, len(dexSet), fmt.Errorf("mobula http %d: %s", resp.StatusCode, truncate(string(body), 200)) - } - var r mobulaResp - if err := json.Unmarshal(body, &r); err != nil { - return total, len(dexSet), fmt.Errorf("mobula parse: %w", err) - } - for _, t := range r.Data { - // Dedupe by tx hash across pages. Providers occasionally - // return the same hash on consecutive pages when a cursor - // resets under the hood. - if _, seen := hashSet[t.TransactionHash]; seen { - continue - } - hashSet[t.TransactionHash] = struct{}{} - total++ - // DEX identity: prefer `platform.name` (mapped protocol), - // fall back to `marketAddress` (raw pool address) so a - // venue with no mapped platform still contributes one - // distinct venue to the coverage-breadth metric instead - // of being dropped silently. - if t.Platform != nil && t.Platform.Name != "" { - dexSet[t.Platform.Name] = struct{}{} - } else if t.MarketAddress != "" { - dexSet[t.MarketAddress] = struct{}{} - } - } - if !r.Pagination.HasMore || r.Pagination.NextCursor == "" || len(r.Data) == 0 { - break - } - cursor = r.Pagination.NextCursor - // Cursor pages back-to-back can trip provider rate limits; a - // short breather keeps the fetch inside its budget without - // starving the cadence. - time.Sleep(50 * time.Millisecond) - } - return total, len(dexSet), nil -} - -// mobulaChainName maps OCB canonical chain slug → Mobula chain param -// value. Kept centralised so the mapping is auditable in one place. -func mobulaChainName(chain string) string { - switch chain { - case "solana": - return "Solana" - case "ethereum": - return "Ethereum" - case "bsc": - return "BNB Smart Chain (BEP20)" - case "base": - return "Base" - case "stellar": - return "Stellar" - default: - return chain - } -} - -func truncate(s string, n int) string { - if len(s) <= n { - return s - } - return s[:n] + "…" -} diff --git a/harnesses/token-trade-coverage/go.mod b/harnesses/token-trade-coverage/go.mod deleted file mode 100644 index 000366ca..00000000 --- a/harnesses/token-trade-coverage/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module github.com/ChainBench/OpenChainBench/harnesses/token-trade-coverage - -go 1.23 - -require github.com/prometheus/client_golang v1.20.5 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - golang.org/x/sys v0.28.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect -) diff --git a/harnesses/token-trade-coverage/go.sum b/harnesses/token-trade-coverage/go.sum deleted file mode 100644 index c231b510..00000000 --- a/harnesses/token-trade-coverage/go.sum +++ /dev/null @@ -1,32 +0,0 @@ -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= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=