Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions benchmarks/token-trade-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# 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 vs Moralis)"
seo_description: "Which onchain data API returns the most complete trade history for a newly launched token? Live coverage % across Mobula, Bitquery, Codex and Moralis 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: draft
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 and Moralis, then computes each provider's capture rate against
the union baseline (max count across the four). 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 four 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, Moralis through the
Solana gateway for Solana and the EVM API for Ethereum, BSC, Base.
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: <API_KEY>`. 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: <API_KEY>`. GraphQL query on `getTokenEvents` with `timestamp` bounds. Cursor pagination."
- "Moralis: `GET https://solana-gateway.moralis.io/token/mainnet/{addr}/swaps` for Solana; `GET https://deep-index.moralis.io/api/v2.2/erc20/{addr}/swaps` for EVM. `X-API-Key` header. Manual cursor pagination, limit=100."
- "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 four 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."
- "{{name:moralis}} averages {{p50:moralis}}. Moralis's Solana coverage is limited to two DEXs (PumpSwap and Meteora Dynamic AMM v2), so on any Solana token that trades across more than those two venues the capture rate drops fast; on EVM chains coverage is broader."
- "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."
- q: "Is Moralis really that far behind on Solana?"
a: "On the Solana row Moralis's rate averages {{p50:moralis:chain:solana}} in this bench. The reason is DEX coverage. Moralis's Solana swaps endpoint at time of writing indexes PumpSwap and Meteora Dynamic AMM v2. Any trade routed through Raydium, Jupiter, Meteora DAMM v1, Orca or the long tail of aggregator DEXs is missed. Providers that index the full DEX set (Mobula, Bitquery, Codex) show much higher capture rates on the same tokens."

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"}

- slug: moralis
name: Moralis
tag: solana-gateway.moralis.io + deep-index.moralis.io
formula: "Trades returned by Moralis per-token swaps endpoint (Solana gateway or EVM API) divided by union-across-providers on (chain, token) rolling 60 min. Excluded from Stellar row."
queries:
p50: avg_over_time(ocb_token_trade_capture_pct{provider="moralis"}[24h])
p90: quantile_over_time(0.90, ocb_token_trade_capture_pct{provider="moralis"}[24h])
p99: quantile_over_time(0.99, ocb_token_trade_capture_pct{provider="moralis"}[24h])
mean: avg_over_time(ocb_token_trade_capture_pct{provider="moralis"}[24h])
success: clamp_max(avg_over_time(ocb_token_trade_probe_ok{provider="moralis"}[24h]), 1)
sample_size: count_over_time(ocb_token_trade_capture_pct{provider="moralis"}[24h])
series: ocb_token_trade_capture_pct{provider="moralis"}
15 changes: 15 additions & 0 deletions harnesses/token-trade-coverage/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# token-trade-coverage harness. Fill in and rename to `.env`.

# Providers (all required for a full bench cycle; the harness
# tolerates missing keys per-provider and reports probe_ok=0 for that
# provider but keeps the others going, so partial config is fine for
# local debugging).
MOBULA_API_KEY=
BITQUERY_API_KEY=
CODEX_API_KEY=
MORALIS_API_KEY=

# Cadence + runtime knobs.
SWEEP_SEC=1800
METRICS_PORT=2112
HTTP_TIMEOUT_SEC=30
12 changes: 12 additions & 0 deletions harnesses/token-trade-coverage/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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"]
98 changes: 98 additions & 0 deletions harnesses/token-trade-coverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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: <key>` | 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: <key>` | 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.
Loading
Loading