diff --git a/benchmarks/app-store-ratings.yml b/benchmarks/app-store-ratings.yml deleted file mode 100644 index cc8e0afb..00000000 --- a/benchmarks/app-store-ratings.yml +++ /dev/null @@ -1,242 +0,0 @@ -# OpenChainBench. Bench 202 - -slug: app-store-ratings -number: "202" -title: "Crypto trading app iOS ratings: Coinbase vs Robinhood vs Crypto.com vs Bybit, live" -seo_title: "Crypto trading app App Store ratings 2026: Coinbase, Robinhood, Crypto.com, Bybit, Kraken, Invo, Fomo, PumpFun, GMGN, Moonshot" -seo_description: "{{best_name}} leads crypto trading apps on the iOS App Store at {{best_p50}} stars. Coinbase vs Robinhood vs Crypto.com vs Bybit vs Kraken vs Invo vs Fomo vs PumpFun vs GMGN vs Moonshot ranked live from iTunes ratings." -subtitle: Live Apple App Store ratings for the leading crypto trading apps on iOS. Updated every 30 minutes from the iTunes lookup API. Rating is the current average across all user reviews. Review count shows how many users voted. - -category: Trading -status: live -metric: App Store rating -unit: x -higher_is_better: true - -disclaimer: | - Ratings reflect the current all-time average user rating from the Apple App Store as returned by the iTunes lookup API. Apple does not expose star-distribution breakdowns or time-windowed averages via the public API. A high rating with few reviews carries more statistical uncertainty than the same rating with tens of thousands of reviews. The review count column shows how many users contributed to each rating. - -seo_intro: | - This benchmark ranks the top crypto trading apps on the iOS App Store by - user rating, updated live every 30 minutes from the iTunes lookup API. It - spans both centralized exchange apps (Coinbase, Robinhood, Crypto.com, Bybit, - Kraken, Binance.US) and on-chain trading terminals (Fomo, GMGN, PumpFun, - Moonshot). Robinhood leads on raw review volume with nearly 5 million ratings, - making it the most statistically reliable data point in the set. Coinbase - follows with over 1.8 million ratings. On-chain apps like Fomo have accumulated - over 7,000 ratings since launching in April 2025, signaling strong early - engagement from DeFi-native traders. - -abstract: | - The harness polls the iTunes public lookup API - (itunes.apple.com/lookup?id=&country=us) every 30 minutes for each - tracked app. The API returns averageUserRating (0-5, rounded to two decimal - places by Apple) and userRatingCount (total all-time reviews). The harness - exposes three gauges per app: app_store_rating, app_store_reviews_total, - and app_store_health. No API key is required. Apple caches the endpoint - but updates it within hours of new reviews arriving. - -methodology: - - "Source: Apple iTunes public lookup API (itunes.apple.com/lookup?id=&country=us). No authentication required. Returns averageUserRating and userRatingCount." - - "Poll cadence: harness fetches each app every 30 minutes. Ratings change slowly (daily to weekly) so 30-minute polling captures all meaningful changes while respecting Apple caching." - - "Rating: Apple computes the average across all all-time user reviews. Apple does not expose time-windowed (e.g. current version only) averages via the public API endpoint used here." - - "Review count: total number of ratings ever submitted on the US App Store. Higher = more statistically reliable average." - - "Robinhood (ID 938003185): the most-reviewed financial app on the US App Store with nearly 5 million ratings. Commission-free investing with crypto added in 2018." - - "Coinbase (ID 886427730): the largest US-regulated crypto exchange. Over 1.8 million App Store ratings." - - "Crypto.com (ID 1262148500): Singapore-headquartered super app with 100M+ users globally, offering spot, derivatives, Visa card and DeFi wallet." - - "Bybit (ID 1488296980): major global exchange strong on derivatives and spot trading." - - "Kraken (ID 1481947260): veteran US exchange with strong security reputation." - - "Binance.US (ID 1492670702): US-regulated arm of Binance, available on the US App Store." - - "Fomo (ID 6741115427): launched Apr 2025. Social copy-trading and meme coin terminal, backed by Index Ventures and USV." - - "GMGN (ID 6745328711): AI-powered trading terminal focused on Solana meme coins." - - "PumpFun (ID 6717572591): launched Oct 2024. Bonding-curve launchpad and trading app for Solana meme coins." - - "Moonshot (ID 6503993131): mobile-first meme coin launchpad with fiat onramp via Apple Pay, by DEX Screener." - - "Invo (ID 1601301148): Hyperliquid-native social trading mobile app with 170+ perp pairs and copy trading via its registered builder code on Hyperliquid." - -findings: - - "{{best_name}} leads crypto trading apps on the iOS App Store at {{best_p50}} stars." - - "Robinhood dominates on review volume with nearly 5 million ratings, making it the most statistically robust data point in the set." - - "Coinbase follows with over 1.8 million ratings and is the top pure-crypto exchange by review count." - - "Fomo has accumulated over 7,000 ratings since its April 2025 launch despite being one of the newest entrants, signaling strong mobile-native engagement." - - "On-chain trading apps (Fomo, GMGN, PumpFun, Moonshot) compete directly with major CEX apps for the same mobile trader." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/app-store-ratings - -prometheus: - window: 24h - freshness_metric: app_store_health - -metric_panels: - - id: reviews - label: Review count - label_key: app - metric: app_store_reviews_total - unit: count - higher_is_better: true - description: "Total number of user ratings submitted on the US App Store. More reviews = more statistically reliable average. Robinhood leads with 4.8M+ ratings, Coinbase follows at 1.8M+." - -ledger_columns: - - { label: "Rating", slot: p50 } - - { label: "Reviews", panel: reviews } - -faq: - - q: "Why does the App Store rating matter for a trading app?" - a: "App Store ratings are the most visible public signal of user satisfaction for mobile products. For trading apps competing for the same users, a higher rating and more reviews signals a better-received product." - - q: "How often do App Store ratings change?" - a: "Slowly. Individual ratings accumulate continuously but the average moves over days to weeks. The harness polls every 30 minutes to capture any meaningful shift; in practice the number updates at most once or twice per day." - - q: "Why is this the US App Store only?" - a: "The iTunes public lookup API returns US store data by default. Review counts and ratings can differ by country. The US store is the largest single market and the standard reference for app comparisons." - - q: "Does this include Google Play ratings?" - a: "No. This benchmark covers the US App Store only. CEX apps like Coinbase and Bybit have Android versions but on-chain apps like Fomo and PumpFun are iOS-only as of 2026." - - q: "Why does Robinhood have so many more reviews than Coinbase?" - a: "Robinhood launched in 2013 as a stock trading app and had 13 million funded accounts before adding crypto in 2018. Its larger and older user base has had more time to leave App Store reviews. Its nearly 5 million ratings make it the most statistically reliable data point in this benchmark." - - q: "What is Crypto.com and why is it included?" - a: "Crypto.com is one of the largest crypto exchanges globally with 100M+ registered users. Its iOS app combines spot trading, a crypto Visa card, DeFi wallet and staking in a single super app, making it a major competitor to Coinbase for everyday crypto users." - - q: "What is Moonshot?" - a: "Moonshot is a mobile-first meme coin launchpad built by DEX Screener, the leading token analytics platform. It differentiates via fiat onboarding through Apple Pay, targeting users who want to buy meme coins without setting up a crypto wallet first." - - q: "Why Binance.US and not Binance?" - a: "The global Binance app is not available on the US App Store due to regulatory restrictions. Binance.US is the separate US-licensed entity with its own app (ID 1492670702) available to American users." - -providers: - - slug: robinhood - name: Robinhood - tag: "US investing app, 4.8M ratings" - formula: "Apple App Store average rating (iTunes lookup API, app ID 938003185, US store, all-time average). Review count from app_store_reviews_total{app=\"robinhood\"}." - queries: - p50: app_store_rating{app="robinhood"} - p90: app_store_rating{app="robinhood"} - p99: app_store_rating{app="robinhood"} - mean: app_store_rating{app="robinhood"} - success: app_store_health{app="robinhood"} - sample_size: app_store_reviews_total{app="robinhood"} - series: app_store_rating{app="robinhood"} - - - slug: coinbase - name: Coinbase - tag: "US crypto exchange" - formula: "Apple App Store average rating (iTunes lookup API, app ID 886427730, US store, all-time average). Review count from app_store_reviews_total{app=\"coinbase\"}." - queries: - p50: app_store_rating{app="coinbase"} - p90: app_store_rating{app="coinbase"} - p99: app_store_rating{app="coinbase"} - mean: app_store_rating{app="coinbase"} - success: app_store_health{app="coinbase"} - sample_size: app_store_reviews_total{app="coinbase"} - series: app_store_rating{app="coinbase"} - - - slug: cryptocom - name: Crypto.com - tag: "Global crypto super app" - formula: "Apple App Store average rating (iTunes lookup API, app ID 1262148500, US store, all-time average). Review count from app_store_reviews_total{app=\"cryptocom\"}." - queries: - p50: app_store_rating{app="cryptocom"} - p90: app_store_rating{app="cryptocom"} - p99: app_store_rating{app="cryptocom"} - mean: app_store_rating{app="cryptocom"} - success: app_store_health{app="cryptocom"} - sample_size: app_store_reviews_total{app="cryptocom"} - series: app_store_rating{app="cryptocom"} - - - slug: bybit - name: Bybit - tag: "Global derivatives exchange" - formula: "Apple App Store average rating (iTunes lookup API, app ID 1488296980, US store, all-time average). Review count from app_store_reviews_total{app=\"bybit\"}." - queries: - p50: app_store_rating{app="bybit"} - p90: app_store_rating{app="bybit"} - p99: app_store_rating{app="bybit"} - mean: app_store_rating{app="bybit"} - success: app_store_health{app="bybit"} - sample_size: app_store_reviews_total{app="bybit"} - series: app_store_rating{app="bybit"} - - - slug: kraken - name: Kraken - tag: "US crypto exchange" - formula: "Apple App Store average rating (iTunes lookup API, app ID 1481947260, US store, all-time average). Review count from app_store_reviews_total{app=\"kraken\"}." - queries: - p50: app_store_rating{app="kraken"} - p90: app_store_rating{app="kraken"} - p99: app_store_rating{app="kraken"} - mean: app_store_rating{app="kraken"} - success: app_store_health{app="kraken"} - sample_size: app_store_reviews_total{app="kraken"} - series: app_store_rating{app="kraken"} - - - slug: fomo - name: Fomo - tag: "Social trading app, Solana" - formula: "Apple App Store average rating (iTunes lookup API, app ID 6741115427, US store, all-time average). Review count from app_store_reviews_total{app=\"fomo\"}." - queries: - p50: app_store_rating{app="fomo"} - p90: app_store_rating{app="fomo"} - p99: app_store_rating{app="fomo"} - mean: app_store_rating{app="fomo"} - success: app_store_health{app="fomo"} - sample_size: app_store_reviews_total{app="fomo"} - series: app_store_rating{app="fomo"} - - - slug: gmgn - name: GMGN - tag: "AI trading terminal, Solana" - formula: "Apple App Store average rating (iTunes lookup API, app ID 6745328711, US store, all-time average). Review count from app_store_reviews_total{app=\"gmgn\"}." - queries: - p50: app_store_rating{app="gmgn"} - p90: app_store_rating{app="gmgn"} - p99: app_store_rating{app="gmgn"} - mean: app_store_rating{app="gmgn"} - success: app_store_health{app="gmgn"} - sample_size: app_store_reviews_total{app="gmgn"} - series: app_store_rating{app="gmgn"} - - - slug: pump-fun - name: PumpFun - tag: "Solana meme launchpad app" - formula: "Apple App Store average rating (iTunes lookup API, app ID 6717572591, US store, all-time average). Review count from app_store_reviews_total{app=\"pump-fun\"}." - queries: - p50: app_store_rating{app="pump-fun"} - p90: app_store_rating{app="pump-fun"} - p99: app_store_rating{app="pump-fun"} - mean: app_store_rating{app="pump-fun"} - success: app_store_health{app="pump-fun"} - sample_size: app_store_reviews_total{app="pump-fun"} - series: app_store_rating{app="pump-fun"} - - - slug: moonshot - name: Moonshot - tag: "Meme launchpad, Apple Pay" - formula: "Apple App Store average rating (iTunes lookup API, app ID 6503993131, US store, all-time average). Review count from app_store_reviews_total{app=\"moonshot\"}." - queries: - p50: app_store_rating{app="moonshot"} - p90: app_store_rating{app="moonshot"} - p99: app_store_rating{app="moonshot"} - mean: app_store_rating{app="moonshot"} - success: app_store_health{app="moonshot"} - sample_size: app_store_reviews_total{app="moonshot"} - series: app_store_rating{app="moonshot"} - - - slug: binance-us - name: Binance.US - tag: "US-regulated Binance exchange" - formula: "Apple App Store average rating (iTunes lookup API, app ID 1492670702, US store, all-time average). Review count from app_store_reviews_total{app=\"binance-us\"}." - queries: - p50: app_store_rating{app="binance-us"} - p90: app_store_rating{app="binance-us"} - p99: app_store_rating{app="binance-us"} - mean: app_store_rating{app="binance-us"} - success: app_store_health{app="binance-us"} - sample_size: app_store_reviews_total{app="binance-us"} - series: app_store_rating{app="binance-us"} - - - slug: invo - name: Invo - tag: "Hyperliquid social trading app" - formula: "Apple App Store average rating (iTunes lookup API, app ID 1601301148, US store, all-time average). Review count from app_store_reviews_total{app=\"invo\"}." - queries: - p50: app_store_rating{app="invo"} - p90: app_store_rating{app="invo"} - p99: app_store_rating{app="invo"} - mean: app_store_rating{app="invo"} - success: app_store_health{app="invo"} - sample_size: app_store_reviews_total{app="invo"} - series: app_store_rating{app="invo"} diff --git a/benchmarks/blast-rpc.yml b/benchmarks/blast-rpc.yml index 899b7b0e..df93cbae 100644 --- a/benchmarks/blast-rpc.yml +++ b/benchmarks/blast-rpc.yml @@ -30,7 +30,7 @@ methodology: findings: - "{{best_name}} currently leads free Blast RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) across 4 measured providers." - - "`rpc.blast.io` posts ~2 ms in every region simultaneously. On-chain response headers confirm geo-distributed nodes: `blast_blast-mainnet_fra` from eu-west, `blast_blast-mainnet_sin` from Singapore, a US node from us-east. Blast runs RPC nodes inside the same GCP datacenters as our Railway probes, producing sub-2 ms figures that reflect co-located infrastructure rather than typical user latency. A user in Paris reaches the Frankfurt node in ~40 ms; a user outside GCP will see 30-100 ms depending on location. Heads are fresh per stale detection." + - "`rpc.blast.io` posts ~2 ms in every region simultaneously, physically impossible for one origin. Read it as an edge-terminated endpoint: heads are fresh per our stale detection, but the latency column measures CDN termination rather than a node round trip, the same class of caution we document for Cloudflare-eth." - "{{name:drpc}} ({{p50:drpc}}) is the honest-infrastructure comparison point: anycast consistency across the three regions with real node round trips behind it, the profile that wins it 10 of the 12 expansion chains." - "{{name:tenderly}} sits at the expansion's familiar flat ~330 ms in all regions on Blast, single-origin routing on a gateway that is genuinely quick on the major chains." @@ -44,7 +44,7 @@ faq: - q: "How is Blast RPC latency measured here?" a: "One identical JSON-RPC POST (`eth_getBlockByNumber`) every 60 seconds against each provider from each of 3 regions, with the same plain HTTP client. Wall-clock round-trip is recorded at millisecond precision; p50/p90/p99 are computed via Prometheus `quantile_over_time` over 24 hours. Responses are classified (`ok` / `http_err` / `jsonrpc_err` / `stale` / `timeout`) so an endpoint stuck on an old head or returning errors behind HTTP 200 is never ranked as fastest. The harness is open source and every number on this page is a public Prometheus query you can run yourself." - q: "Is rpc.blast.io really that fast, or is something else going on?" - a: "Blast runs geo-distributed RPC nodes inside major cloud datacenters (GCP us-east, GCP europe-west4/Frankfurt, GCP asia-southeast1/Singapore). Our Railway probes run in the same three GCP regions, so each probe hits the co-located Blast node with a sub-LAN round trip. The `x-node-id` response header confirms this: `blast_blast-mainnet_fra` from eu-west, `blast_blast-mainnet_sin` from Singapore. The blocks are fresh — this is not a cache hit — but the 1-2 ms figure reflects network co-location, not what a developer outside GCP infra will experience. From a typical residential connection or a non-GCP cloud, expect 30-100 ms." + a: "Something else. Two milliseconds simultaneously from Virginia, Amsterdam and Singapore is below the physical round-trip floor for any single origin, so the endpoint is answering at an anycast/CDN edge. Our stale-head detection shows the blocks it returns are current, so it is not serving a stale cache today, but edge termination means the latency figure describes the edge network, not node processing. We keep it ranked with the caveat documented, exactly as we do for Cloudflare's fast-but-permissioned Ethereum endpoint." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/rpc-capabilities diff --git a/benchmarks/bridge-fee.yml b/benchmarks/bridge-fee.yml index db25d8a0..accdd3da 100644 --- a/benchmarks/bridge-fee.yml +++ b/benchmarks/bridge-fee.yml @@ -121,7 +121,7 @@ prometheus: # rank_matrix_query required when dimensions.region is declared. # label_replace maps bridge= → provider= so badge scoping works per region. -rank_matrix_query: avg by (provider, region) (label_replace(quantile_over_time(0.50, bridge_cost_percent{from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h]), "provider", "$1", "bridge", "(.+)")) +rank_matrix_query: avg by (provider, region) (label_replace(quantile_over_time(0.50, bridge_cost_percent[24h]), "provider", "$1", "bridge", "(.+)")) providers: - slug: mobula @@ -130,23 +130,23 @@ providers: formula: "Per-corridor 24h median of total cost percent (fees + slippage + destination gas) on a $300 USDC quote from Mobula's intent layer, averaged across corridors. Sampled every 5 minutes from eu-west." type: intent queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="mobula", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[1h]) - slug: relay name: Relay @@ -154,23 +154,23 @@ providers: formula: "Per-corridor 24h median of total cost percent (fees + slippage + destination gas) on a $300 USDC quote from Relay's API, averaged across corridors. Sampled every 5 minutes from eu-west." type: relay queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="relay", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[1h]) - slug: lifi name: LI.FI @@ -178,23 +178,23 @@ providers: formula: "Per-corridor 24h median of total cost percent (underlying bridge fee + slippage + destination gas + LI.FI markup) on a $300 USDC quote, averaged across corridors. Sampled every 5 minutes from eu-west." type: aggregator queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="lifi", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[1h]) - slug: debridge name: deBridge @@ -202,23 +202,23 @@ providers: formula: "Per-corridor 24h median of total cost percent (native-token protocol fee + slippage + destination gas) on a $300 USDC quote from deBridge's direct API, averaged across corridors. Sampled every 5 minutes from eu-west." type: protocol queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="debridge", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[1h]) - slug: across name: Across @@ -226,23 +226,23 @@ providers: formula: "Per-corridor 24h median of total cost percent on a $300 USDC quote from the keyless Across Swap API, averaged across corridors. The provider-computed USD fee total nets relayer capital, LP fee, destination gas and swap impact." type: intent queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="across", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[1h]) - slug: near-intents name: Near Intents @@ -250,23 +250,23 @@ providers: formula: "Per-corridor 24h median of total cost percent on a $300 USDC quote from the Near Intents 1Click API, averaged across corridors. The quoted amountOut already nets out solver spread, bridge fee and destination withdrawal fee." type: intent queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="near-intents", from_token!="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", region="sgp"}[1h]) - slug: squid name: Squid Router @@ -274,23 +274,23 @@ providers: formula: "Per-corridor 24h median of total cost percent on a $300 USDC quote from Squid Router's v2 API, averaged across supported EVM corridors (Base to Arb). Cost = fromAmountUSD minus toAmountUSD including all relayer and gas fees." type: aggregator queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="squid", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="squid", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="squid", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="squid", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="squid"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="squid"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="squid"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="squid"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="squid"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="squid"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="squid", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="squid", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="squid", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="squid", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="squid", region="sgp"}[1h]) - slug: socket name: Socket Protocol @@ -298,20 +298,20 @@ providers: formula: "All-in cost percent p50 over 24h on a $300 USDC best-output route via Socket Protocol's v2 API (routes through Across, Stargate, CCTP), EVM corridors only (Base to Arb)." type: aggregator queries: - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", amount_usd="300"}[24h])) - p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="socket", amount_usd="300"}[24h])) - p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="socket", amount_usd="300"}[24h])) - mean: avg(avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300"}[24h])) - success: avg(avg_over_time(bridge_quote_success{bridge="socket", amount_usd="300"}[24h])) - sample_size: sum(count_over_time(bridge_cost_percent{bridge="socket", amount_usd="300"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_cost_percent{bridge="socket"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_cost_percent{bridge="socket"}[24h])) + mean: avg(avg_over_time(bridge_cost_percent{bridge="socket"}[24h])) + success: avg(avg_over_time(bridge_quote_success{bridge="socket"}[24h])) + sample_size: sum(count_over_time(bridge_cost_percent{bridge="socket"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="socket"}[1h]) regions: - region: eu-west - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", amount_usd="300", region="eu-west"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300", region="eu-west"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", region="eu-west"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="socket", region="eu-west"}[1h]) - region: us-east - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", amount_usd="300", region="us-east"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300", region="us-east"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", region="us-east"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="socket", region="us-east"}[1h]) - region: sgp - p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", amount_usd="300", region="sgp"}[24h])) - series: avg_over_time(bridge_cost_percent{bridge="socket", amount_usd="300", region="sgp"}[1h]) + p50: avg(quantile_over_time(0.50, bridge_cost_percent{bridge="socket", region="sgp"}[24h])) + series: avg_over_time(bridge_cost_percent{bridge="socket", region="sgp"}[1h]) diff --git a/benchmarks/l1-finality.yml b/benchmarks/l1-finality.yml index b8889763..73ba0d3c 100644 --- a/benchmarks/l1-finality.yml +++ b/benchmarks/l1-finality.yml @@ -4,7 +4,7 @@ slug: l1-finality number: "006" title: Fastest L1 blockchain finality, live across 11 chains seo_title: "Fastest L1 finality 2026" -seo_description: "{{best_name}} leads L1 finality at {{best_p50}} (p50, 24h) across 11 chains. Ethereum, Solana, Bitcoin, SUI, Gram: live wall-clock measurements, not whitepaper claims." +seo_description: "Fastest L1 blockchain finality measured live across 11 chains: Gram, SUI, Hedera, Stellar, Solana, Ethereum, Bitcoin and more. Real times, not whitepaper claims." subtitle: Wall-clock seconds from latest block to the finalized block on Ethereum, Solana, Bitcoin, Gram, SUI, Stellar and 5 more chains, refreshed every 10 seconds. seo_intro: | This page measures L1 finality time live for every major Layer-1 blockchain, with p50 / p90 / p99 refreshed every 10 seconds. Stellar finality time is ~5 seconds, the close interval the Stellar Consensus Protocol locks in via federated Byzantine agreement. Solana finality time goes from sub-second on the processed commitment to ~12.8 s on finalized after 32 confirmed slots. Ethereum finality time is about 12.8 minutes in the ideal case, the 2-epoch Casper FFG window; observed wall-clock finality runs longer, and the table below shows the live measurement. Hedera finality time clears in 3-5 seconds via Hashgraph aBFT. SUI finality time and Gram finality time (formerly TON) both sit under one second via Mysticeti DAG-BFT and BAG consensus. BNB and Avalanche finality time land near two seconds through fast-finality forks. Probabilistic chains (Bitcoin, Litecoin, Monero) settle on a confirmation-depth convention measured here in minutes. Bitcoin finality time is roughly one hour at the industry-standard 6 confirmations; the protocol itself never reaches absolute finality. diff --git a/benchmarks/memecoin-platforms.yml b/benchmarks/memecoin-platforms.yml deleted file mode 100644 index cb651588..00000000 --- a/benchmarks/memecoin-platforms.yml +++ /dev/null @@ -1,177 +0,0 @@ -# OpenChainBench. Bench 203 - -slug: memecoin-platforms -number: "203" -title: "Solana memecoin platform fee rates: Axiom vs Fomo vs pump.fun vs GMGN vs Trojan, live 24h" -seo_title: "Solana memecoin platform fees 2026: Axiom, Fomo, pump.fun, GMGN, Trojan, Photon, Maestro" -seo_description: "{{best_name}} leads Solana memecoin platforms at {{best_p50}} take rate. Axiom vs Fomo vs pump.fun vs GMGN vs Trojan ranked by protocol revenue per dollar traded, live from Dune fee wallet monitoring." -subtitle: Live 24-hour protocol fee revenue divided by trading volume for each Solana memecoin platform. Fees sourced from Dune Analytics (on-chain fee wallet inflows, refreshed every 6 hours). Volume from Mobula lighthouse (refreshed every 15 minutes). - -category: Trading -status: live -metric: 24h take rate -unit: pct -higher_is_better: false - -disclaimer: | - Take rate = fees / volume * 100. Fees are on-chain inflows to known fee wallets (Dune query 8316589, refreshed every 6h). Fomo fees come from DeFiLlama (on-chain USDC + off-chain relay). Volume is total Mobula-attributed volume; the take rate is a lower bound since platforms with variable or waivable fees will show lower observed rates. - -seo_intro: | - Solana memecoin platforms charge fees in different ways and at different rates, - making direct comparison difficult without on-chain data. This benchmark - measures the actual protocol revenue collected over 24 hours by monitoring - known fee wallets on Dune Analytics, then divides by the total trading volume - attributed to each platform by Mobula's lighthouse aggregator. Axiom leads on - absolute protocol revenue, consistent with its 0.9% per-trade fee applied - across high-volume memecoin flow. The observed take rate for GMGN and Trojan - is lower than their stated fee because Mobula's volume attribution is broader - than the set of trades on which the platforms actually charge. pump.fun cut its - bonding-curve trading fees to 0% in August 2026, so its observed take rate has - dropped near zero; protocol revenue now comes from graduation fees only. - -abstract: | - Fee data: Dune query 8316589 monitors native SOL inflows (via solana.account_activity, - address column) and USDC/WSOL inflows (via token_balance_owner column) to known - protocol fee wallets for each platform, summed over a rolling 24-hour window. - Executed every 6 hours (costs Dune credits); cached results read every 15 minutes. - Fomo exception: Fomo combines on-chain USDC sweeps with off-chain relay fees - stored in a private Dune table accessible only to DeFiLlama; fees for Fomo come - from DeFiLlama /summary/fees/fomo chainBreakdown[Solana]. Volume data: Mobula - lighthouse (api.mobula.io/api/2/market/lighthouse) polled every 15 minutes. - Trading platforms from byPlatform; pump.fun from byLaunchpad[name=PumpFun]. - Two take-rate metrics: fee_rate_pct = fees / Mobula total volume (lower bound); - fee_paying_rate_pct = fees / fee-paying volume (Dune tx join to dex_solana.trades, - comparable across platforms). coverage_pct = fee-paying / Mobula volume. - -methodology: - - "Fee source (all except Fomo): Dune Analytics query 8316589 on solana.account_activity. Native SOL inflows matched by address = fee wallet; USDC/WSOL inflows matched by token_balance_owner = fee wallet. Summed over NOW() - 1 DAY. Executed every 6h; cached result read every 15min." - - "Fee source (Fomo): DeFiLlama /summary/fees/fomo, chainBreakdown[Solana].total24h. DeFiLlama combines on-chain USDC inflows to R4rNJHaffSUotNmqSKNEfDcJE8A7zJUkaoM5Jkd7cYX and off-chain relay fees from dune.tryfomo.fomo_relay_fees (private Dune table)." - - "Volume source (Mobula): Mobula lighthouse byPlatform for trading terminals; byLaunchpad[name=PumpFun] for pump.fun (bonding curve + PumpSwap graduated AMM). Updated every 15 minutes. This is total attributed volume; includes trades that may not have generated a fee." - - "Volume source (fee-paying, Dune): For each transaction that generated a native SOL, USDC, or WSOL inflow to a fee wallet, the largest swap trade in dex_solana.trades with matching tx_id is taken (MAX amount_usd per tx, to avoid double-counting multi-hop routes). Sum across all such transactions = fee_paying_volume_usd_24h. This denominator is self-consistent with the fee numerator." - - "Two take-rate metrics: memecoin_platform_fee_rate_pct = fees / Mobula total volume (lower bound, shows attribution breadth); memecoin_platform_fee_paying_rate_pct = fees / fee-paying volume (comparable across platforms, closer to the actual per-trade fee rate). memecoin_platform_coverage_pct = fee-paying volume / Mobula volume (shows what fraction of attributed volume generated a fee)." - - "SOL price: CoinGecko simple/price, refreshed every 5 minutes. Used to convert native SOL and WSOL inflows to USD." - - "GMGN: 9 fee wallets verified by DeFiLlama adapter (master, 2026-06) as pure collectors sweeping to a single treasury. DeFiLlama Solana-only fees match ours within 5%, confirming the wallet list is complete. GMGN's $772K/day DeFiLlama total includes BSC, Base, Ethereum, Monad and other EVM chains; this bench is Solana-only." - - "pump.fun: fees captured via native SOL and WSOL inflows to the 9 known protocol fee wallets. pump.fun cut its bonding-curve trading fees to 0% in August 2026; observed fee revenue post that date reflects token graduation fees only. Creator fees (0.30%) and LP fees flow to separate recipients not tracked here." - - "Trojan: fee wallets capture the per-swap protocol transfer; DeFiLlama Solana match at 6% confirms fees are in the swap transaction, not a separate settlement." - -findings: - - "{{best_name}} has the highest observed take rate at {{best_p50}} over the last 24h." - - "Axiom leads on absolute protocol revenue at roughly $800K per day, consistent with its 0.9% stated fee applied across high-volume memecoin flow." - - "Using the self-consistent fee-paying volume denominator (Dune tx join), Axiom shows ~1.25%, GMGN ~1.73%, Trojan ~1.35%, and Maestro ~1.87%, clustered closer to stated rates than the Mobula-volume figures, which are depressed by volume attribution breadth." - - "pump.fun cut its bonding-curve trading fees to 0% in August 2026; its observed take rate has dropped near zero as protocol fee revenue is now only graduation fees. Creator and LP fee shares flow to separate recipients." - - "GMGN and Trojan show ~8% coverage (fee-paying volume / Mobula volume), meaning only 8% of attributed volume generates a fee inflow. Their Dune fee captures match DeFiLlama Solana within 5-6%, confirming the wallet list is complete." - - "Fomo fees include both on-chain USDC sweeps and off-chain relay fees; the latter are not directly capturable via Dune without access to Fomo's private data feed." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/memecoin-platforms - -prometheus: - window: 24h - freshness_metric: memecoin_platform_health - -faq: - - q: "What is a take rate?" - a: "Take rate is the fraction of trading volume captured as protocol revenue: fees_usd_24h / volume_usd_24h * 100. A platform with a 1% per-trade fee and full fee coverage on all attributed volume would show a 1% take rate. Lower rates indicate either a lower stated fee, waived fees on some trades, or a volume denominator that includes trades where no fee was charged." - - q: "Why is GMGN's take rate 0.14% if they charge 1%?" - a: "GMGN charges 1% per trade, and their fee wallets do collect ~$155K/day on Solana, matching DeFiLlama's Solana-only figure within 5%. But Mobula's lighthouse attributes $108M in volume to GMGN, which is broader than the volume on which GMGN actually collects a fee. The 0.14% figure is the ratio of fees to total attributed volume, not the per-trade fee rate. GMGN's total revenue across all chains (BSC, Base, Ethereum, Monad, etc.) is ~$772K/day per DeFiLlama." - - q: "Why does pump.fun show a near-zero take rate?" - a: "pump.fun cut its bonding-curve trading fees to 0% in August 2026, eliminating its per-swap protocol revenue. The Dune fee wallet monitors now capture primarily token graduation fees rather than trading fees. The byLaunchpad volume denominator remains high ($100M+), so the computed take rate is near zero. Pre-August 2026, pump.fun ran a 0.95% protocol share on buys and sells, producing ~0.45% observed take rate." - - q: "Why is Fomo sourced from DeFiLlama instead of Dune directly?" - a: "Fomo collects fees in two ways: on-chain USDC sweeps to a fee wallet, and off-chain relay fees reported to a private Dune table (dune.tryfomo.fomo_relay_fees) that only Fomo's team can write to. DeFiLlama has access to this private table via a partnership. Without it, on-chain monitoring alone would capture only a fraction of Fomo's revenue." - - q: "How reliable are the fee wallet addresses?" - a: "GMGN's 9 Solana wallets are verified by DeFiLlama's adapter (reviewed 2026-06) as pure fee collectors with no inter-wallet flows. Our 5% match to DeFiLlama Solana confirms the list is current. Axiom, Trojan, Photon, and Maestro wallets were identified by reverse-engineering tagged trades and cross-checked against DeFiLlama. A monthly diff of the DeFiLlama adapter detects any rotation." - - q: "What is memecoin_platform_fee_paying_rate_pct and how does it differ from fee_rate_pct?" - a: "memecoin_platform_fee_rate_pct uses Mobula's total attributed volume as the denominator, which is a lower bound because Mobula attributes more trades to each platform than those that actually paid a fee. memecoin_platform_fee_paying_rate_pct uses only the volume of trades that generated a fee inflow (from Dune: for each fee-receiving transaction, we look up the largest swap in dex_solana.trades via tx_id match and sum those). This denominator is self-consistent with the fee numerator, so the resulting rate is comparable across platforms and approximates the actual per-trade fee. memecoin_platform_coverage_pct shows what fraction of Mobula volume actually generated a fee." - - q: "What is memecoin_dune_data_freshness_seconds?" - a: "It is the Unix timestamp of the most recent block_time seen in the Dune query results. If Dune's solana.account_activity table lags, the nominal 24-hour window silently shrinks. A gap of more than 4 hours between this metric and the current time indicates stale data." - -providers: - - slug: axiom - name: Axiom - tag: "Solana trading terminal, 0.9% fee" - formula: "24h protocol fee revenue (Dune wallet inflows) / 24h fee-paying volume (Dune tx join). Fees: native SOL inflows to 20 known fee wallets. Fee-paying volume: largest swap in each fee-receiving tx from dex_solana.trades." - queries: - p50: memecoin_platform_fee_paying_rate_pct{platform="axiom"} - p90: memecoin_platform_fee_paying_rate_pct{platform="axiom"} - p99: memecoin_platform_fee_paying_rate_pct{platform="axiom"} - mean: memecoin_platform_fee_paying_rate_pct{platform="axiom"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="axiom"} - series: memecoin_platform_fee_paying_rate_pct{platform="axiom"} - - - slug: fomo - name: Fomo - tag: "Solana mobile trading app" - formula: "24h protocol fee revenue (DeFiLlama Solana) / 24h volume (Mobula lighthouse). Fomo fees include on-chain USDC + off-chain relay fees via DeFiLlama; fee-paying volume not available for Fomo." - queries: - p50: memecoin_platform_fee_rate_pct{platform="fomo"} - p90: memecoin_platform_fee_rate_pct{platform="fomo"} - p99: memecoin_platform_fee_rate_pct{platform="fomo"} - mean: memecoin_platform_fee_rate_pct{platform="fomo"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="fomo"} - series: memecoin_platform_fee_rate_pct{platform="fomo"} - - - slug: pump-fun - name: pump.fun - tag: "Solana meme launchpad + PumpSwap AMM" - formula: "24h protocol fee revenue (Dune wallet inflows) / 24h fee-paying volume (Dune tx join). Captures protocol share (0.95%) on bonding curve and PumpSwap trades; creator and LP shares excluded." - queries: - p50: memecoin_platform_fee_paying_rate_pct{platform="pump-fun"} - p90: memecoin_platform_fee_paying_rate_pct{platform="pump-fun"} - p99: memecoin_platform_fee_paying_rate_pct{platform="pump-fun"} - mean: memecoin_platform_fee_paying_rate_pct{platform="pump-fun"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="pump-fun"} - series: memecoin_platform_fee_paying_rate_pct{platform="pump-fun"} - - - slug: gmgn - name: GMGN - tag: "Solana AI trading terminal, 1% fee" - formula: "24h Solana protocol fee revenue (Dune, 9 wallet addresses) / 24h fee-paying volume (Dune tx join). Total revenue across all chains is ~5x higher per DeFiLlama." - queries: - p50: memecoin_platform_fee_paying_rate_pct{platform="gmgn"} - p90: memecoin_platform_fee_paying_rate_pct{platform="gmgn"} - p99: memecoin_platform_fee_paying_rate_pct{platform="gmgn"} - mean: memecoin_platform_fee_paying_rate_pct{platform="gmgn"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="gmgn"} - series: memecoin_platform_fee_paying_rate_pct{platform="gmgn"} - - - slug: photon - name: Photon - tag: "Solana trading terminal, ~1% fee" - formula: "24h protocol fee revenue (Dune wallet inflows) / 24h fee-paying volume (Dune tx join)." - queries: - p50: memecoin_platform_fee_paying_rate_pct{platform="photon"} - p90: memecoin_platform_fee_paying_rate_pct{platform="photon"} - p99: memecoin_platform_fee_paying_rate_pct{platform="photon"} - mean: memecoin_platform_fee_paying_rate_pct{platform="photon"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="photon"} - series: memecoin_platform_fee_paying_rate_pct{platform="photon"} - - - slug: trojan - name: Trojan - tag: "Solana Telegram bot, ~0.9% fee" - formula: "24h protocol fee revenue (Dune wallet inflows) / 24h fee-paying volume (Dune tx join). Fee is collected in the swap transaction; DeFiLlama Solana match within 6% confirms capture." - queries: - p50: memecoin_platform_fee_paying_rate_pct{platform="trojan"} - p90: memecoin_platform_fee_paying_rate_pct{platform="trojan"} - p99: memecoin_platform_fee_paying_rate_pct{platform="trojan"} - mean: memecoin_platform_fee_paying_rate_pct{platform="trojan"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="trojan"} - series: memecoin_platform_fee_paying_rate_pct{platform="trojan"} - - - slug: maestro - name: Maestro - tag: "Solana Telegram bot, ~1% fee" - formula: "24h protocol fee revenue (Dune wallet inflows) / 24h fee-paying volume (Dune tx join)." - queries: - p50: memecoin_platform_fee_paying_rate_pct{platform="maestro"} - p90: memecoin_platform_fee_paying_rate_pct{platform="maestro"} - p99: memecoin_platform_fee_paying_rate_pct{platform="maestro"} - mean: memecoin_platform_fee_paying_rate_pct{platform="maestro"} - success: memecoin_platform_health - sample_size: memecoin_platform_fees_usd_24h{platform="maestro"} - series: memecoin_platform_fee_paying_rate_pct{platform="maestro"} diff --git a/benchmarks/monad-rpc.yml b/benchmarks/monad-rpc.yml index 0f4885d8..8a14e1a5 100644 --- a/benchmarks/monad-rpc.yml +++ b/benchmarks/monad-rpc.yml @@ -47,7 +47,7 @@ methodology: findings: - "{{best_name}} currently leads the free Monad RPC field at {{best_p50}} (p50, 24h) across 5 measured providers." - - "{{name:monad-official}} answers at {{p50:monad-official}}. On-chain response headers (`x-node-id: monad_monad-mainnet_fra`) confirm geo-distributed nodes co-located with our Railway probes in GCP datacenters (Frankfurt for eu-west, equivalent US and Singapore nodes). The sub-2 ms figure reflects network co-location, not what a developer outside GCP infra will experience — expect 30-120 ms from a typical connection. Rate-limit: 25 rps." + - "{{name:monad-official}} answers at {{p50:monad-official}}. The Foundation fronts its primary endpoint with QuickNode infrastructure and rate-limits at 25 rps, generous for development traffic." - "{{name:drpc}} sits at {{p50:drpc}} and {{name:tenderly}} at {{p50:tenderly}}: the multi-chain gateways reached Monad within weeks of mainnet, but route through their global meshes rather than chain-local nodes." - "{{name:bloxroute}} at {{p50:bloxroute}} is the outlier entry: a BDN operator exposing a keyless endpoint, unusual for a company whose core product is paid low-latency access." diff --git a/benchmarks/perp-cost-slope.yml b/benchmarks/perp-cost-slope.yml index f8787ac6..ee8f3802 100644 --- a/benchmarks/perp-cost-slope.yml +++ b/benchmarks/perp-cost-slope.yml @@ -87,8 +87,9 @@ per_chain_explainer: or higher. findings: - - "{{name:gains}} and {{name:gmx}} always score 1.00 by construction, not by performance: oracle-priced venues charge a flat percentage of notional with no orderbook, so the $1M all-in is identical to the $1k all-in in basis points regardless of market conditions. Their top leaderboard position reflects fee structure, not execution quality." - - "Among orderbook venues, {{name:hyperliquid}} sits at {{p50:hyperliquid}} cost slope (24h avg) — the lowest impact in the cohort at $1M notional, driven by the deepest onchain book." + - "{{best_name}} leads the cost-slope leaderboard at {{best_p50}} (24h average). A slope of 1.00 means the venue charges the same basis points per unit of notional at $1M as at $1k." + - "{{name:gains}} and {{name:gmx}} are the only venues in the cohort that structurally score 1.00: they use oracle pricing with no orderbook, so the fee is a flat percentage of position size regardless of trade size." + - "{{name:hyperliquid}} sits at {{p50:hyperliquid}} cost slope (24h avg). The deepest onchain orderbook in the cohort keeps its impact low, but it still exceeds 1.00 at $1M notional on most assets." - "Venues showing N/A in the slope column cannot reliably fill a $1M market order. The fill-rate panel shows what fraction of measurement cycles the $1M tier was successfully absorbed." - "The cost slope is asset-specific: use the chain tabs to compare ETH (deepest books), BTC (tightest books) and SOL (thinnest books) separately." diff --git a/benchmarks/perp-protocol-longevity.yml b/benchmarks/perp-protocol-longevity.yml index f17f01b3..efedea82 100644 --- a/benchmarks/perp-protocol-longevity.yml +++ b/benchmarks/perp-protocol-longevity.yml @@ -59,7 +59,7 @@ methodology: - "Incident criteria: direct theft or permanent loss of user funds via a documented smart contract vulnerability or privileged key compromise. Oracle manipulation without contract exploit is excluded. Market-structure events (liquidation cascades, large-position forced close) are excluded." - "gains.trade: launched 2021-12-01. No incidents in tracked sources as of registry date 2026-08-02. Clean streak = days since 2021-12-01." - "GMX v2: launched 2023-08-01. Incident 2025-07-09: reentrancy in vault and order-flow logic, estimated loss USD 42 000 000, source coinperps.xyz post-mortem. Clean streak = days since 2025-07-09." - - "Hyperliquid: launched 2023-11-01. No protocol exploit in tracked sources. Note: the March 2025 JellyJelly forced close was a market-structure event, not a smart contract exploit, and is excluded per criteria. DeFiLlama enrichment filters entries with no confirmed financial loss (amount null or 0) to avoid false positives such as unrelated projects that share a name substring." + - "Hyperliquid: launched 2023-11-01. No protocol exploit in tracked sources. Note: the March 2025 JellyJelly forced close was a market-structure event (large-position liquidation cascade triggering a governance emergency), not a smart contract exploit, and is excluded per criteria." - "dYdX v4: launched on the Cosmos appchain 2023-10-01. No protocol exploit on v4. dYdX v1 and v2 on Ethereum had oracle manipulation events that are excluded here (different contract, different era)." - "Lighter: launched 2023-07-01. No incidents in tracked sources." - "Paradex: launched 2023-10-01. No incidents in tracked sources." diff --git a/benchmarks/solana-avg-trade-size.yml b/benchmarks/solana-avg-trade-size.yml deleted file mode 100644 index b2e22708..00000000 --- a/benchmarks/solana-avg-trade-size.yml +++ /dev/null @@ -1,198 +0,0 @@ -# OpenChainBench. Bench 206 - -slug: solana-avg-trade-size -number: "206" -title: "Average swap size on Solana: pump.fun vs GMGN vs Fomo vs Axiom, live" -seo_title: "Average trade size by Solana trading platform 2026: pump.fun vs GMGN vs Axiom vs Fomo" -seo_description: "{{best_name}} has the largest average swap size at ${{best_p50}} per trade. pump.fun vs GMGN vs Axiom vs Fomo vs Trojan ranked by average trade size, live from Mobula lighthouse." -subtitle: Live average swap size in USD per attributed transaction for each Solana trading platform and launchpad. Polled every 15 minutes from Mobula lighthouse. Reveals the trader profile of each platform, from retail micro-trades to sophisticated large-order flow. - -category: Trading -status: live -metric: Avg swap size -unit: usd -higher_is_better: true - -disclaimer: | - Average swap size is computed as 24h attributed volume divided by 24h attributed trade count from Mobula lighthouse. Both numerator and denominator include bot and MEV activity; the metric reflects all on-chain swaps attributed to each platform, not human-only trades. pump.fun volume comes from the byLaunchpad bucket (bonding-curve swaps via pump.fun's own frontend); all other platforms come from byPlatform. Attribution is mutually exclusive per Mobula's referral-tag methodology. - -seo_intro: | - The average swap size on Solana varies by several multiples across platforms, - revealing sharply different trader profiles. pump.fun consistently sits at the - bottom of the leaderboard, reflecting the micro-cap degen activity of its - bonding-curve launchpad and the heavy bot sniping that inflates trade count while - compressing average size. GMGN and Trojan serve more sophisticated traders running - larger positions and consistently rank at the top. Fomo sits in the middle, - positioning between retail and professional. This benchmark tracks average swap - size live across all major Solana trading frontends and launchpads, updated every - 15 minutes from Mobula lighthouse attribution data. - -abstract: | - The harness polls Mobula lighthouse (api.mobula.io/api/2/market/lighthouse) every - 15 minutes and records volumeUSD.24h and trades.24h for each platform and launchpad. - Average swap size is derived as volumeUSD.24h divided by trades.24h. The harness - exposes solana_platform_volume_24h_usd, solana_platform_trades_24h and - solana_platform_health per platform. pump.fun is sourced from byLaunchpad and - cross-published into the platform gauge; all others come from byPlatform. - -methodology: - - "Source: Mobula lighthouse (api.mobula.io/api/2/market/lighthouse), poll every 15 min." - - "Average swap size: solana_platform_volume_24h_usd / solana_platform_trades_24h, computed in PromQL." - - "Attribution: Mobula assigns each on-chain swap to a platform via referral tags, program IDs and memo fields embedded in transactions. Each swap is attributed to exactly one platform." - - "Bot inclusion: all attributed swaps are counted including bots and MEV. Platforms with high bot activity (notably pump.fun snipers) show lower average sizes than human-only baselines would suggest." - - "pump.fun: sourced from byLaunchpad (bonding-curve swaps on pump.fun's own site). Trades routed through GMGN or Axiom to pump.fun tokens count under those platforms." - - "Meteora DBC excluded: Meteora DBC counts LP provision events and micro-rebalances as trades, making its avg size of ~$12 not comparable to pure swap activity." - -findings: - - "{{best_name}} attracts the largest average swap at ${{best_p50}} per transaction." - - "pump.fun has the smallest average swap size in the set, dominated by bot sniping and micro-cap degen retail flow on its bonding-curve launchpad." - - "GMGN and Trojan consistently lead on average swap size, serving more sophisticated traders with larger position sizes." - - "Fomo sits mid-market between retail launchpad activity and professional terminals." - - "Trade count tells the other story: pump.fun processes more trades per day than all other platforms combined." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-launchpad-wars - -prometheus: - window: 24h - freshness_metric: solana_platform_trades_24h - -faq: - - q: "What does average swap size measure?" - a: "Average swap size is total 24h attributed volume divided by total 24h attributed trade count. It reflects the typical notional size of a single swap on each platform. A platform with many small trades will show a low average; one with fewer but larger trades will show a high average." - - q: "Why does pump.fun have so many trades?" - a: "pump.fun's bonding curve generates enormous transaction volume from sniping bots, copy-trade bots and retail buyers all racing to buy newly launched tokens in the first seconds. The 3.7 million daily trades include a large proportion of automated activity, which also drags the average size down toward $35." - - q: "Is this apple-to-apple between pump.fun and the other platforms?" - a: "Directionally yes. Mobula attributes each swap to exactly one frontend using referral tags. A pump.fun token bought via GMGN counts under GMGN, not pump.fun. So both reflect genuine frontend-attributed activity. The difference is that pump.fun's activity skews heavily toward bots and micro-trades on bonding-curve launches, while GMGN and Axiom serve more secondary-market trading." - - q: "Why is Fomo's average trade size lower than GMGN despite similar positioning?" - a: "Fomo attracts a broader retail base through its social copy-trading and wallet features, which pulls the average down compared to GMGN's power-trader demographic. Fomo also has notably low protocol fees in Mobula's data relative to DeFiLlama, suggesting part of its fee model may not be captured by on-chain referral attribution." - -providers: - - slug: pump-fun - name: pump.fun - tag: "Solana meme launchpad" - formula: "Average swap size in USD on pump.fun's bonding curve (solana_platform_volume_24h_usd{platform=pump-fun} / solana_platform_trades_24h{platform=pump-fun}, Mobula lighthouse byLaunchpad, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="pump-fun"} / solana_platform_trades_24h{platform="pump-fun"} - p90: solana_platform_volume_24h_usd{platform="pump-fun"} / solana_platform_trades_24h{platform="pump-fun"} - p99: solana_platform_volume_24h_usd{platform="pump-fun"} / solana_platform_trades_24h{platform="pump-fun"} - mean: solana_platform_volume_24h_usd{platform="pump-fun"} / solana_platform_trades_24h{platform="pump-fun"} - success: solana_platform_health{platform="pump-fun"} - sample_size: solana_platform_trades_24h{platform="pump-fun"} - series: solana_platform_volume_24h_usd{platform="pump-fun"} / solana_platform_trades_24h{platform="pump-fun"} - - - slug: gmgn - name: GMGN - tag: "AI trading terminal on Solana" - formula: "Average swap size in USD routed through GMGN (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="gmgn"} / solana_platform_trades_24h{platform="gmgn"} - p90: solana_platform_volume_24h_usd{platform="gmgn"} / solana_platform_trades_24h{platform="gmgn"} - p99: solana_platform_volume_24h_usd{platform="gmgn"} / solana_platform_trades_24h{platform="gmgn"} - mean: solana_platform_volume_24h_usd{platform="gmgn"} / solana_platform_trades_24h{platform="gmgn"} - success: solana_platform_health{platform="gmgn"} - sample_size: solana_platform_trades_24h{platform="gmgn"} - series: solana_platform_volume_24h_usd{platform="gmgn"} / solana_platform_trades_24h{platform="gmgn"} - - - slug: trojan - name: Trojan - tag: "Telegram trading bot on Solana" - formula: "Average swap size in USD routed through Trojan (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="trojan"} / solana_platform_trades_24h{platform="trojan"} - p90: solana_platform_volume_24h_usd{platform="trojan"} / solana_platform_trades_24h{platform="trojan"} - p99: solana_platform_volume_24h_usd{platform="trojan"} / solana_platform_trades_24h{platform="trojan"} - mean: solana_platform_volume_24h_usd{platform="trojan"} / solana_platform_trades_24h{platform="trojan"} - success: solana_platform_health{platform="trojan"} - sample_size: solana_platform_trades_24h{platform="trojan"} - series: solana_platform_volume_24h_usd{platform="trojan"} / solana_platform_trades_24h{platform="trojan"} - - - slug: maestro - name: Maestro - tag: "Telegram trading bot on Solana" - formula: "Average swap size in USD routed through Maestro (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="maestro"} / solana_platform_trades_24h{platform="maestro"} - p90: solana_platform_volume_24h_usd{platform="maestro"} / solana_platform_trades_24h{platform="maestro"} - p99: solana_platform_volume_24h_usd{platform="maestro"} / solana_platform_trades_24h{platform="maestro"} - mean: solana_platform_volume_24h_usd{platform="maestro"} / solana_platform_trades_24h{platform="maestro"} - success: solana_platform_health{platform="maestro"} - sample_size: solana_platform_trades_24h{platform="maestro"} - series: solana_platform_volume_24h_usd{platform="maestro"} / solana_platform_trades_24h{platform="maestro"} - - - slug: fomo - name: Fomo - tag: "Solana trading platform" - formula: "Average swap size in USD routed through Fomo (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="fomo"} / solana_platform_trades_24h{platform="fomo"} - p90: solana_platform_volume_24h_usd{platform="fomo"} / solana_platform_trades_24h{platform="fomo"} - p99: solana_platform_volume_24h_usd{platform="fomo"} / solana_platform_trades_24h{platform="fomo"} - mean: solana_platform_volume_24h_usd{platform="fomo"} / solana_platform_trades_24h{platform="fomo"} - success: solana_platform_health{platform="fomo"} - sample_size: solana_platform_trades_24h{platform="fomo"} - series: solana_platform_volume_24h_usd{platform="fomo"} / solana_platform_trades_24h{platform="fomo"} - - - slug: axiom - name: Axiom - tag: "Web trading terminal on Solana" - formula: "Average swap size in USD routed through Axiom (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="axiom"} / solana_platform_trades_24h{platform="axiom"} - p90: solana_platform_volume_24h_usd{platform="axiom"} / solana_platform_trades_24h{platform="axiom"} - p99: solana_platform_volume_24h_usd{platform="axiom"} / solana_platform_trades_24h{platform="axiom"} - mean: solana_platform_volume_24h_usd{platform="axiom"} / solana_platform_trades_24h{platform="axiom"} - success: solana_platform_health{platform="axiom"} - sample_size: solana_platform_trades_24h{platform="axiom"} - series: solana_platform_volume_24h_usd{platform="axiom"} / solana_platform_trades_24h{platform="axiom"} - - - slug: photon - name: Photon - tag: "Solana trading terminal" - formula: "Average swap size in USD routed through Photon (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="photon"} / solana_platform_trades_24h{platform="photon"} - p90: solana_platform_volume_24h_usd{platform="photon"} / solana_platform_trades_24h{platform="photon"} - p99: solana_platform_volume_24h_usd{platform="photon"} / solana_platform_trades_24h{platform="photon"} - mean: solana_platform_volume_24h_usd{platform="photon"} / solana_platform_trades_24h{platform="photon"} - success: solana_platform_health{platform="photon"} - sample_size: solana_platform_trades_24h{platform="photon"} - series: solana_platform_volume_24h_usd{platform="photon"} / solana_platform_trades_24h{platform="photon"} - - - slug: padre - name: Padre - tag: "AI Telegram bot on Solana" - formula: "Average swap size in USD routed through Padre (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="padre"} / solana_platform_trades_24h{platform="padre"} - p90: solana_platform_volume_24h_usd{platform="padre"} / solana_platform_trades_24h{platform="padre"} - p99: solana_platform_volume_24h_usd{platform="padre"} / solana_platform_trades_24h{platform="padre"} - mean: solana_platform_volume_24h_usd{platform="padre"} / solana_platform_trades_24h{platform="padre"} - success: solana_platform_health{platform="padre"} - sample_size: solana_platform_trades_24h{platform="padre"} - series: solana_platform_volume_24h_usd{platform="padre"} / solana_platform_trades_24h{platform="padre"} - - - slug: phantom - name: Phantom - tag: "Solana wallet, DEX aggregator" - formula: "Average swap size in USD routed through Phantom wallet (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="phantom"} / solana_platform_trades_24h{platform="phantom"} - p90: solana_platform_volume_24h_usd{platform="phantom"} / solana_platform_trades_24h{platform="phantom"} - p99: solana_platform_volume_24h_usd{platform="phantom"} / solana_platform_trades_24h{platform="phantom"} - mean: solana_platform_volume_24h_usd{platform="phantom"} / solana_platform_trades_24h{platform="phantom"} - success: solana_platform_health{platform="phantom"} - sample_size: solana_platform_trades_24h{platform="phantom"} - series: solana_platform_volume_24h_usd{platform="phantom"} / solana_platform_trades_24h{platform="phantom"} - - - slug: bloom - name: Bloom - tag: "Solana trading terminal" - formula: "Average swap size in USD routed through Bloom (Mobula lighthouse byPlatform, 15 min poll)." - queries: - p50: solana_platform_volume_24h_usd{platform="bloom"} / solana_platform_trades_24h{platform="bloom"} - p90: solana_platform_volume_24h_usd{platform="bloom"} / solana_platform_trades_24h{platform="bloom"} - p99: solana_platform_volume_24h_usd{platform="bloom"} / solana_platform_trades_24h{platform="bloom"} - mean: solana_platform_volume_24h_usd{platform="bloom"} / solana_platform_trades_24h{platform="bloom"} - success: solana_platform_health{platform="bloom"} - sample_size: solana_platform_trades_24h{platform="bloom"} - series: solana_platform_volume_24h_usd{platform="bloom"} / solana_platform_trades_24h{platform="bloom"} diff --git a/benchmarks/solana-dex-volume.yml b/benchmarks/solana-dex-volume.yml deleted file mode 100644 index 1d77fd23..00000000 --- a/benchmarks/solana-dex-volume.yml +++ /dev/null @@ -1,200 +0,0 @@ -# OpenChainBench. Bench 205 - -slug: solana-dex-volume -number: "205" -title: "Solana DEX volume: pump.fun vs Axiom vs GMGN vs Fomo, live 24h" -seo_title: "Solana DEX trading volume 2026: pump.fun vs Axiom vs GMGN vs Fomo vs Trojan vs Photon vs BullX" -seo_description: "{{best_name}} leads Solana DEX trading platforms at ${{best_p50}} 24h volume. pump.fun vs Axiom vs GMGN vs Fomo vs Trojan vs Photon vs BullX ranked live from DeFiLlama." -subtitle: Live 24-hour trading volume and protocol revenue for the top Solana DEX platforms. Updated every 30 minutes from DeFiLlama. The fee column shows what each protocol earns, not what users pay in gas. - -category: Trading -status: live -metric: 24h volume -unit: usd -higher_is_better: true - -disclaimer: | - Volume and revenue data sourced from DeFiLlama's DEX and fees APIs. DeFiLlama attributes volume via on-chain referral tags and program IDs; platforms without standard tags may be undercounted. Figures are 24-hour and 7-day rolling totals updated every 30 minutes. Revenue uses DeFiLlama's dailyRevenue dataType, stripping LP fees. Terminal volume (Axiom, GMGN, Fomo) and launchpad volume (pump.fun) use different attribution methods and are not directly additive. - -seo_intro: | - The Solana DEX market in summer 2026 is split between pump.fun's launchpad, - which dominates early-stage token trading, and a set of competing terminals - (Axiom, GMGN, Fomo) targeting active traders on graduated tokens. pump.fun - cut its bonding-curve trading fees to zero on August 7 2026 in response to - Fomo briefly overtaking Axiom in daily revenue the day before, reshaping the - competitive landscape overnight. Axiom and GMGN each process around $70 - million per day in terminal volume. Fomo reached $50 million daily volume and - crossed Axiom on revenue for the first time on August 6 2026. This benchmark - tracks all seven platforms in real time from DeFiLlama's DEX data feed, - refreshed every 30 minutes. - -abstract: | - The harness polls DeFiLlama's public DEX API (api.llama.fi/summary/dexs/{slug}) - and fees API (api.llama.fi/summary/fees/{slug}?dataType=dailyRevenue) every 30 - minutes for each tracked platform. No API key is required. DeFiLlama attributes - swap volume using on-chain referral tags and program IDs embedded in transactions. - The harness exposes six Prometheus gauges per platform: defillama_dex_volume_24h_usd, - defillama_dex_volume_7d_usd, defillama_dex_fees_24h_usd, defillama_dex_fees_7d_usd, - defillama_dex_take_rate and defillama_dex_health. - -methodology: - - "Source: DeFiLlama public DEX API (api.llama.fi/summary/dexs/{slug}) and fees API (api.llama.fi/summary/fees/{slug}?dataType=dailyRevenue). No authentication required." - - "Poll cadence: harness fetches each platform every 30 minutes and updates Prometheus gauges immediately." - - "Volume: total24h and total7d fields from DeFiLlama's DEX endpoint, sum of all swap notional values in USD attributed to this platform." - - "Revenue: total24h and total7d from the fees endpoint with dataType=dailyRevenue. This strips LP fees that stay in pool accounts and returns only the protocol's own share." - - "Take rate: protocol revenue divided by trading volume (defillama_dex_fees_24h_usd / defillama_dex_volume_24h_usd). Measures how much of each dollar traded the platform captures." - - "Attribution: DeFiLlama uses on-chain referral tags, program IDs and memo fields. Platforms without standard tagging may be undercounted." - - "pump.fun (slug pump.fun, layer Launchpad): the original bonding-curve launchpad. Users trade tokens directly on the curve; a token graduates to PumpSwap's AMM at roughly $69K market cap. Cut bonding-curve fees to zero on Aug 7 2026." - - "Axiom (slug axiom, layer Terminal): web-based Solana trading terminal. 0.25 to 0.5% platform fee on routed swaps. Revenue briefly overtaken by Fomo on Aug 6 2026." - - "GMGN (slug gmgn, layer Terminal): AI-powered trading terminal on Solana. Approximately 1% fee on routed swaps." - - "fomo Wallet (slug fomo-wallet, layer Terminal): Fomo's DEX routing layer. 0.5% platform fee. First crossed Axiom on daily revenue Aug 6 2026." - - "Trojan (slug trojan, layer Bot): Telegram trading bot on Solana. Lower volume than web terminals but serves a distinct Telegram-native user base." - - "Photon (slug photon, layer Terminal): Solana trading terminal targeted at advanced traders." - - "BullX (slug bullx, layer Terminal): Solana and multi-chain trading terminal. Volume fell sharply from its 2025 highs after competitors expanded; now a fraction of peak levels." - - "PumpSwap is pump.fun's backend AMM where graduated tokens trade. It is not tracked here because its volume overlaps with terminal volume: when Axiom or GMGN route a swap to PumpSwap, that trade appears in both the terminal's figures and PumpSwap's. Tracking PumpSwap alongside the terminals would double-count activity." - - "Phantom: Phantom Wallet is the largest Solana wallet by user count but does not have its own DeFiLlama DEX slug. Phantom routes swaps through Jupiter and other aggregators, which appear in those protocols' volume figures." - -findings: - - "{{best_name}} leads Solana DEX platforms at ${{best_p50}} 24h volume." - - "pump.fun dominates early-stage token volume with its bonding-curve launchpad, even after cutting trading fees to zero on Aug 7 2026." - - "Axiom and GMGN are the leading independent trading terminals, each routing around $70 million per day on graduated tokens." - - "Fomo briefly outpaced Axiom in daily revenue on Aug 6 2026, the event that triggered pump.fun's zero-fee response the following day." - - "The take rate column reveals the business model split: pump.fun earns on token launches and lifecycle fees, while Axiom, GMGN and Fomo charge frontend routing fees on each swap." - - "BullX is tracked but operates at a fraction of peak volume following market share losses in 2026." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-dex-volume - -prometheus: - window: 24h - freshness_metric: defillama_dex_volume_24h_usd - -metric_panels: - - id: fees - label: Protocol revenue (24h) - label_key: platform - metric: defillama_dex_fees_24h_usd - unit: usd - higher_is_better: true - description: "Protocol revenue captured by each platform in the last 24 hours (DeFiLlama dailyRevenue). Excludes LP fees that stay in pool accounts." - - - id: volume_7d - label: 7-day volume - label_key: platform - metric: defillama_dex_volume_7d_usd - unit: usd - higher_is_better: true - description: "Total DEX trading volume over the last 7 days." - - - id: take_rate - label: Take rate - label_key: platform - metric: defillama_dex_take_rate - unit: pct - higher_is_better: false - description: "Protocol revenue as a fraction of trading volume (revenue / volume). Higher means the platform captures more of each dollar traded." - -ledger_columns: - - { label: "24h Volume", slot: p50 } - - { label: "24h Revenue", panel: fees } - - { label: "7d Volume", panel: volume_7d } - -faq: - - q: "Why is PumpSwap not tracked here?" - a: "PumpSwap is pump.fun's backend AMM where tokens trade after graduating from the bonding curve. It is excluded because its volume overlaps with the terminal volume already tracked: when Axiom, GMGN or Fomo routes a swap to PumpSwap, that trade appears in both the terminal's DeFiLlama figures and PumpSwap's. Including PumpSwap alongside the terminals would double-count that activity and distort the comparison. The bench tracks platforms where users go directly to trade, not the underlying AMM they route through." - - q: "What is Fomo?" - a: "Fomo (fomo.family) is a Solana trading platform and wallet launched in April 2025 by a team backed by Index Ventures and USV. It charges 0.5% on routed swaps and briefly led Solana daily DEX revenue on August 6 2026, overtaking Axiom for the first time." - - q: "Why did pump.fun cut its fees to zero?" - a: "On August 6 2026, Fomo briefly overtook Axiom in daily protocol revenue, signaling that frontend trading terminals were capturing an increasing share of the Solana DEX market. pump.fun responded on August 7 by eliminating bonding-curve trading fees, making it cheaper to launch and trade new tokens on pump.fun than on competing platforms." - - q: "Why is Phantom not tracked here?" - a: "Phantom is the largest Solana wallet by user count but it routes swaps through aggregators like Jupiter rather than operating its own DEX contract. DeFiLlama does not assign Phantom a DEX slug because its on-chain footprint is the aggregator's, not Phantom's. Those swaps appear in Jupiter's figures. This bench tracks the platforms that DeFiLlama can attribute volume to directly." - - q: "How is this different from the Solana trading platform leaderboard (bench 201)?" - a: "Bench 201 uses Mobula's lighthouse API to attribute swaps to the frontend UI (GMGN, Axiom, Fomo, etc.). This bench uses DeFiLlama's protocol-level DEX data, which tracks on-chain referral tags and program IDs. The two data sources cross-validate each other and may differ when users route through multiple aggregators." - - q: "Why does Photon show less volume than GMGN and Axiom?" - a: "Photon targets advanced traders and has a smaller but higher-frequency user base. Volume varies by day and market conditions. DeFiLlama attribution may also undercount platforms that do not embed standard on-chain referral tags." - - q: "What is the take rate and why does it matter?" - a: "The take rate is protocol revenue divided by trading volume. It shows how much of each dollar traded the platform keeps. pump.fun has a high take rate because it earns creator fees, graduation fees and protocol fees across the token lifecycle. Terminals like Axiom, GMGN and Fomo earn a routing fee on each swap. Comparing take rates reveals which platforms extract more value per dollar of activity." - -providers: - - slug: pump-fun - name: pump.fun - tag: "Launchpad, Solana" - formula: "24h DEX trading volume in USD for pump.fun bonding curve (DeFiLlama summary/dexs/pump.fun, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="pump-fun"} - p90: defillama_dex_volume_24h_usd{platform="pump-fun"} - p99: defillama_dex_volume_24h_usd{platform="pump-fun"} - mean: defillama_dex_volume_24h_usd{platform="pump-fun"} - success: defillama_dex_health{platform="pump-fun"} - series: defillama_dex_volume_24h_usd{platform="pump-fun"} - - - slug: axiom - name: Axiom - tag: "Terminal, Solana" - formula: "24h DEX trading volume in USD for Axiom (DeFiLlama summary/dexs/axiom, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="axiom"} - p90: defillama_dex_volume_24h_usd{platform="axiom"} - p99: defillama_dex_volume_24h_usd{platform="axiom"} - mean: defillama_dex_volume_24h_usd{platform="axiom"} - success: defillama_dex_health{platform="axiom"} - series: defillama_dex_volume_24h_usd{platform="axiom"} - - - slug: gmgn - name: GMGN - tag: "Terminal, Solana" - formula: "24h DEX trading volume in USD for GMGN (DeFiLlama summary/dexs/gmgn, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="gmgn"} - p90: defillama_dex_volume_24h_usd{platform="gmgn"} - p99: defillama_dex_volume_24h_usd{platform="gmgn"} - mean: defillama_dex_volume_24h_usd{platform="gmgn"} - success: defillama_dex_health{platform="gmgn"} - series: defillama_dex_volume_24h_usd{platform="gmgn"} - - - slug: fomo - name: Fomo - tag: "Terminal, Solana" - formula: "24h DEX trading volume in USD for Fomo Wallet (DeFiLlama summary/dexs/fomo-wallet, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="fomo"} - p90: defillama_dex_volume_24h_usd{platform="fomo"} - p99: defillama_dex_volume_24h_usd{platform="fomo"} - mean: defillama_dex_volume_24h_usd{platform="fomo"} - success: defillama_dex_health{platform="fomo"} - series: defillama_dex_volume_24h_usd{platform="fomo"} - - - slug: trojan - name: Trojan - tag: "Bot, Telegram" - formula: "24h DEX trading volume in USD for Trojan (DeFiLlama summary/dexs/trojan, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="trojan"} - p90: defillama_dex_volume_24h_usd{platform="trojan"} - p99: defillama_dex_volume_24h_usd{platform="trojan"} - mean: defillama_dex_volume_24h_usd{platform="trojan"} - success: defillama_dex_health{platform="trojan"} - series: defillama_dex_volume_24h_usd{platform="trojan"} - - - slug: photon - name: Photon - tag: "Terminal, Solana" - formula: "24h DEX trading volume in USD for Photon (DeFiLlama summary/dexs/photon, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="photon"} - p90: defillama_dex_volume_24h_usd{platform="photon"} - p99: defillama_dex_volume_24h_usd{platform="photon"} - mean: defillama_dex_volume_24h_usd{platform="photon"} - success: defillama_dex_health{platform="photon"} - series: defillama_dex_volume_24h_usd{platform="photon"} - - - slug: bullx - name: BullX - tag: "Terminal, multi-chain" - formula: "24h DEX trading volume in USD for BullX (DeFiLlama summary/dexs/bullx, total24h, updated every 30 min)." - queries: - p50: defillama_dex_volume_24h_usd{platform="bullx"} - p90: defillama_dex_volume_24h_usd{platform="bullx"} - p99: defillama_dex_volume_24h_usd{platform="bullx"} - mean: defillama_dex_volume_24h_usd{platform="bullx"} - success: defillama_dex_health{platform="bullx"} - series: defillama_dex_volume_24h_usd{platform="bullx"} diff --git a/benchmarks/solana-launchpad-wars.yml b/benchmarks/solana-launchpad-wars.yml deleted file mode 100644 index 627b3d2c..00000000 --- a/benchmarks/solana-launchpad-wars.yml +++ /dev/null @@ -1,176 +0,0 @@ -# OpenChainBench. Bench 200 - -slug: solana-launchpad-wars -number: "200" -title: "Solana meme launchpad leaderboard: PumpFun vs Flap vs Bankr, live 24h volume" -seo_title: "Solana meme launchpad comparison 2026: PumpFun, Flap, Bankr, LetsCash" -seo_description: "{{best_name}} leads Solana meme launchpads at ${{best_p50}} 24h volume. PumpFun vs Flap vs Bankr vs LetsCash vs Meteora DBC vs NOXA Fun ranked live from Mobula lighthouse data." -subtitle: Live 24-hour trading volume for each Solana meme coin launchpad. Polled every 15 minutes from Mobula lighthouse, which aggregates on-chain swap events across all bonding-curve programs on Solana. - -category: Trading -status: live -metric: 24h volume -unit: usd -higher_is_better: true - -disclaimer: | - Volume figures are sourced from Mobula's lighthouse aggregator, which indexes on-chain swap events. Data is refreshed every 15 minutes. A launchpad with zero volume may indicate no activity in the last 24 hours or a temporary data gap from Mobula. Fees shown are protocol-side fees detected on-chain; off-chain relay or referral fees are excluded. - -seo_intro: | - Solana's meme coin launchpad market is dominated by PumpFun but is far from a - monopoly. Flap, Bankr, LetsCash, NOXA Fun and Meteora DBC all run active - bonding-curve programs on Solana competing for the same launch traffic. This - benchmark tracks the 24-hour trading volume, protocol fees and trade count for - each platform in real time. PumpFun consistently leads with over 100 million - USD in daily volume. In August 2026 PumpFun cut its bonding-curve trading fees - to zero, so its daily protocol fee revenue now comes primarily from token - graduations rather than per-swap charges. Flap and Meteora DBC occupy the - challenger positions. The fee efficiency column (fees divided by volume) - reveals which launchpad extracts the most revenue per dollar traded. - -abstract: | - The harness polls the Mobula lighthouse endpoint (api.mobula.io/api/2/market/lighthouse) - every 15 minutes. Mobula aggregates all on-chain swap events into rolling windows - (15 min, 1 h, 6 h, 24 h) per launchpad and per trading platform. The harness - exposes three gauges per launchpad: solana_launchpad_volume_24h_usd, - solana_launchpad_fees_24h_usd and solana_launchpad_trades_24h. All launchpads - returned by the API are tracked automatically. The benchmark YAML selects the - eight highest-volume Solana-native launchpads as providers. - -methodology: - - "Source: Mobula lighthouse (api.mobula.io/api/2/market/lighthouse), byLaunchpad field. Mobula indexes all Solana bonding-curve programs and aggregates swap events into 24h rolling windows updated approximately every minute on Mobula's side." - - "Poll cadence: harness fetches the endpoint every 15 minutes and updates Prometheus gauges. Prometheus scrapes :2112/metrics every 60 seconds." - - "Volume: sum of all swap event notional values (in USD at time of swap) within the last 24 hours for swaps routed through this launchpad's program. Higher = more active market." - - "Fees: on-chain protocol fees collected by the launchpad program within the last 24 hours. Off-chain relay fees and referral fees are not included." - - "Trades: count of individual swap events within the last 24 hours." - - "Launchpad scope: this bench covers bonding-curve launchpads (PumpFun, Flap, Bankr, LetsCash, NOXA Fun, Meteora DBC, Raydium Launchlab). Trading frontends that route through multiple launchpads (GMGN, Axiom, Fomo, Trojan) are a separate bench." - - "PumpFun note: PumpFun volume here is bonding-curve activity only. Tokens that graduate from the bonding curve to PumpSwap AMM are counted separately under PumpSwap in the DEX view and are not included here." - -findings: - - "{{best_name}} leads Solana meme launchpads at ${{best_p50}} 24h volume." - - "PumpFun commands the market with over $100 million in daily bonding-curve volume, roughly 70 percent of total tracked launchpad volume. Protocol fee revenue is now primarily from token graduations after cutting bonding-curve trading fees to zero in August 2026." - - "Flap and Meteora DBC are the closest challengers, each processing tens of millions in daily volume. Flap has the highest fee-to-volume ratio among active launchpads." - - "Bankr, LetsCash and NOXA Fun each run active programs with single-digit millions in daily volume and tens of thousands in daily fees." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-launchpad-wars - -prometheus: - window: 24h - freshness_metric: solana_launchpad_volume_24h_usd - -faq: - - q: "Which Solana meme launchpad has the most volume?" - a: "PumpFun leads with over $100 million in 24h bonding-curve volume, roughly 70 percent of total tracked launchpad volume. Flap and Meteora DBC are the closest challengers. The leaderboard updates every 15 minutes from live Mobula lighthouse data." - - q: "What counts as launchpad volume here?" - a: "On-chain swap events routed through the launchpad's bonding-curve program, aggregated over a rolling 24-hour window by Mobula. For PumpFun this is the bonding-curve phase only; tokens that have graduated to the PumpSwap AMM are not included in this figure." - - q: "How does PumpFun compare to Flap on fees?" - a: "PumpFun cut its bonding-curve trading fees to zero in August 2026, so its daily protocol fee revenue now comes almost entirely from graduation fees rather than per-swap charges. Flap charges a higher per-trade fee rate. The solana_launchpad_fees_24h_usd gauge tracks live fees for each launchpad." - - q: "What is Meteora DBC?" - a: "Meteora Dynamic Bonding Curve is Meteora's launchpad product on Solana, competing directly with PumpFun. It uses a configurable bonding curve and routes graduated tokens into Meteora's DLMM liquidity pools." - - q: "Are trading bots like GMGN and Axiom included?" - a: "No. GMGN, Axiom, Fomo and Trojan are trading frontends that route orders through multiple launchpads and DEXes. Their volume is attributable to the underlying programs, not to a single launchpad. A separate bench tracks the trading platform leaderboard." - -providers: - - slug: pumpfun - name: PumpFun - tag: "Solana launchpad, #1 by volume" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=PumpFun].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} - p90: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} - p99: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} - mean: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} - success: solana_launchpad_health{launchpad="pumpfun"} - sample_size: solana_launchpad_trades_24h{launchpad="pumpfun"} - series: solana_launchpad_volume_24h_usd{launchpad="pumpfun"} - - - slug: flap - name: Flap - tag: "Solana launchpad, top fee rate" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Flap].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="flap"} - p90: solana_launchpad_volume_24h_usd{launchpad="flap"} - p99: solana_launchpad_volume_24h_usd{launchpad="flap"} - mean: solana_launchpad_volume_24h_usd{launchpad="flap"} - success: solana_launchpad_health{launchpad="flap"} - sample_size: solana_launchpad_trades_24h{launchpad="flap"} - series: solana_launchpad_volume_24h_usd{launchpad="flap"} - - - slug: meteora-dbc - name: Meteora DBC - tag: "Meteora DBC, Solana launchpad" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Meteora DBC].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} - p90: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} - p99: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} - mean: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} - success: solana_launchpad_health{launchpad="meteora-dbc"} - sample_size: solana_launchpad_trades_24h{launchpad="meteora-dbc"} - series: solana_launchpad_volume_24h_usd{launchpad="meteora-dbc"} - - - slug: noxa-fun - name: NOXA Fun - tag: "Solana meme launchpad" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=NOXA Fun].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} - p90: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} - p99: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} - mean: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} - success: solana_launchpad_health{launchpad="noxa-fun"} - sample_size: solana_launchpad_trades_24h{launchpad="noxa-fun"} - series: solana_launchpad_volume_24h_usd{launchpad="noxa-fun"} - - - slug: bankr - name: Bankr - tag: "Solana meme launchpad" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Bankr].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="bankr"} - p90: solana_launchpad_volume_24h_usd{launchpad="bankr"} - p99: solana_launchpad_volume_24h_usd{launchpad="bankr"} - mean: solana_launchpad_volume_24h_usd{launchpad="bankr"} - success: solana_launchpad_health{launchpad="bankr"} - sample_size: solana_launchpad_trades_24h{launchpad="bankr"} - series: solana_launchpad_volume_24h_usd{launchpad="bankr"} - - - slug: letscash - name: LetsCash - tag: "Solana meme launchpad" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=LetsCash].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="letscash"} - p90: solana_launchpad_volume_24h_usd{launchpad="letscash"} - p99: solana_launchpad_volume_24h_usd{launchpad="letscash"} - mean: solana_launchpad_volume_24h_usd{launchpad="letscash"} - success: solana_launchpad_health{launchpad="letscash"} - sample_size: solana_launchpad_trades_24h{launchpad="letscash"} - series: solana_launchpad_volume_24h_usd{launchpad="letscash"} - - - slug: dontblink - name: dontblink - tag: "Solana meme launchpad" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=dontblink].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="dontblink"} - p90: solana_launchpad_volume_24h_usd{launchpad="dontblink"} - p99: solana_launchpad_volume_24h_usd{launchpad="dontblink"} - mean: solana_launchpad_volume_24h_usd{launchpad="dontblink"} - success: solana_launchpad_health{launchpad="dontblink"} - sample_size: solana_launchpad_trades_24h{launchpad="dontblink"} - series: solana_launchpad_volume_24h_usd{launchpad="dontblink"} - - - slug: raydium-launchlab - name: Raydium Launchlab - tag: "Raydium launchpad on Solana" - formula: "24h bonding-curve swap volume in USD. Source: Mobula lighthouse byLaunchpad[name=Raydium Launchlab].volumeUSD.24h, updated every 15 min." - queries: - p50: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} - p90: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} - p99: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} - mean: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} - success: solana_launchpad_health{launchpad="raydium-launchlab"} - sample_size: solana_launchpad_trades_24h{launchpad="raydium-launchlab"} - series: solana_launchpad_volume_24h_usd{launchpad="raydium-launchlab"} diff --git a/benchmarks/solana-trading-platform-wars.yml b/benchmarks/solana-trading-platform-wars.yml deleted file mode 100644 index fe2b82d6..00000000 --- a/benchmarks/solana-trading-platform-wars.yml +++ /dev/null @@ -1,185 +0,0 @@ -# OpenChainBench. Bench 201 - -slug: solana-trading-platform-wars -number: "201" -title: "Solana trading platform leaderboard: pump.fun vs GMGN vs Axiom vs Fomo, live 24h volume" -seo_title: "Solana trading platform comparison 2026: pump.fun, GMGN, Axiom, Fomo, Trojan" -seo_description: "{{best_name}} leads Solana trading platforms at ${{best_p50}} 24h volume. pump.fun vs GMGN vs Axiom vs Fomo vs Trojan vs Padre vs Phantom ranked live from Mobula lighthouse data." -subtitle: Live 24-hour trading volume for each Solana trading platform and Telegram bot. Polled every 15 minutes from Mobula lighthouse, which aggregates on-chain swap events routed through each frontend across all Solana DEX programs. - -category: Trading -status: live -metric: 24h volume -unit: usd -higher_is_better: true - -disclaimer: | - Volume figures are sourced from Mobula's lighthouse aggregator, which indexes on-chain swap events attributed to each trading platform or Telegram bot. Data is refreshed every 15 minutes. Platforms with zero volume may indicate no attributable on-chain activity in the last 24 hours or a temporary attribution gap in Mobula's indexing. - -seo_intro: | - Solana's trading frontend market is a contested race between GMGN, Axiom and - Fomo, with Trojan and Padre close behind as the dominant Telegram bots. This - benchmark tracks the 24-hour trading volume routed through each platform in - real time using Mobula's lighthouse aggregator, which attributes on-chain - swap events to the originating frontend. GMGN consistently ranks at or near - the top, regularly processing over $100 million in daily volume on active - days. Axiom competes closely behind and the two regularly trade places as - market conditions shift. Fomo has been narrowing the gap and on active days - has matched or exceeded Axiom, making the top three increasingly competitive. - Trojan and Padre serve the Telegram-native trader segment, each capturing - significant bot-driven flow. Phantom routes wallet-initiated swaps through - its interface, representing users who prefer the wallet UI over dedicated - trading terminals. The fee column shows which platforms capture the most - protocol revenue per dollar traded; see methodology for Fomo's fee source. - -abstract: | - The harness polls the Mobula lighthouse endpoint (api.mobula.io/api/2/market/lighthouse) - every 15 minutes. Mobula attributes each on-chain swap event to the originating - trading platform or bot using referral tags, program IDs and memo fields embedded - in transactions. The harness exposes three gauges per platform: - solana_platform_volume_24h_usd, solana_platform_fees_24h_usd and - solana_platform_health. All platforms returned by the API are tracked - automatically. The benchmark YAML selects the highest-volume Solana-native - trading platforms as providers. - -methodology: - - "Source: Mobula lighthouse (api.mobula.io/api/2/market/lighthouse), byPlatform field. Mobula attributes swap events using on-chain referral tags, program IDs and memo fields." - - "Poll cadence: harness fetches the endpoint every 15 minutes and updates Prometheus gauges. Prometheus scrapes :2112/metrics every 60 seconds." - - "Volume: sum of all swap event notional values (in USD at time of swap) within the last 24 hours for swaps attributed to this platform. Higher = more user activity routed through this frontend." - - "Attribution: Mobula uses on-chain referral tags, program IDs and memo fields. Each swap is attributed to exactly one platform. Platforms without standard referral tags may be undercounted." - - "pump.fun: sourced from Mobula's byLaunchpad bucket (bonding-curve swaps via pump.fun's own frontend). PumpSwap trades routed through GMGN or Axiom count under those platforms." - - "Phantom: wallet-initiated swaps via Jupiter or other aggregators, not active trading sessions." - -findings: - - "{{best_name}} leads Solana trading platforms at ${{best_p50}} 24h volume." - - "GMGN and Axiom consistently rank at the top of the leaderboard and regularly trade places. Both serve power traders with web-based terminals; GMGN emphasizes AI copy-trading while Axiom focuses on execution speed." - - "Fomo has been closing the gap on Axiom and on competitive days overtakes it, making the top three a genuinely contested race rather than a fixed hierarchy." - - "Trojan and Padre serve the Telegram-native segment and together account for a significant fraction of bot-driven Solana swap flow." - - "Phantom is the dominant Solana wallet by installs; its volume here represents wallet-initiated swaps via Jupiter, a different usage pattern than active trading sessions." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-trading-platform-wars - -prometheus: - window: 24h - freshness_metric: solana_platform_volume_24h_usd - -faq: - - q: "Which Solana trading platform has the most volume?" - a: "GMGN and Axiom consistently trade places at the top, and Fomo has been narrowing the gap. The leaderboard is live and updates every 15 minutes, so the current ranking is always visible at the top of the page. Headlines claiming one platform has overtaken another are often based on a single day or a specific metric; this bench tracks the rolling 24h window continuously." - - q: "Does the pump.fun volume here include PumpSwap?" - a: "Partially. The pump.fun line comes from Mobula's byLaunchpad bucket, which covers bonding-curve swaps via pump.fun's own frontend. PumpSwap (the AMM for graduated tokens) is a separate program launched in 2025. Direct PumpSwap trades via pump.fun's interface may be partially included depending on Mobula's indexing of the PumpSwap program. Trades on pump.fun tokens routed through GMGN or Axiom to PumpSwap count under those platforms, not under pump.fun." - - q: "What is the difference between GMGN and Axiom?" - a: "Both are web-based Solana trading terminals targeting power traders. GMGN emphasizes AI-powered wallet analysis and copy trading. Axiom is a terminal-style platform focused on chart speed and order execution. Both route through Jupiter and direct DEX pools." - - q: "Why does Fomo's fee column use a different source?" - a: "Fomo collects protocol fees in USDC swept in batches to a dedicated fee wallet, separate from individual swap transactions. Mobula's attribution tracks fees via referral tags embedded per trade and misses the batch sweeps, understating Fomo fees by roughly 9x. The fee column for Fomo uses DeFiLlama, which monitors the Fomo fee wallet directly and captures the full revenue. All other platforms use Mobula." - - q: "Are Trojan and Padre Telegram bots?" - a: "Yes. Trojan and Padre are Telegram-native trading bots that execute Solana swaps on behalf of users. They embed referral tags in transactions which Mobula uses to attribute volume." - - q: "How is trading platform volume different from launchpad volume?" - a: "Launchpad volume (bench 200) counts swaps through bonding-curve programs like pump.fun or Flap. Platform volume here counts swaps attributed to a trading frontend regardless of which underlying DEX or launchpad was used. Attribution is mutually exclusive per Mobula's referral tag system: a trade on pump.fun tokens routed through GMGN counts under GMGN, not under pump.fun." - - q: "Why does BullX show zero or low volume?" - a: "BullX has limited Solana attribution in Mobula's lighthouse data. BullX primarily targets EVM chains and its Solana swap volume may not be tagged with referral data that Mobula can attribute. DEX volume for BullX across chains is tracked in bench 205." - -providers: - - slug: pump-fun - name: pump.fun - tag: "Solana meme launchpad, bonding curve" - formula: "24h swap volume in USD on pump.fun's bonding curve via pump.fun's own frontend (Mobula lighthouse byLaunchpad[name=PumpFun].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="pump-fun"} - p90: solana_platform_volume_24h_usd{platform="pump-fun"} - p99: solana_platform_volume_24h_usd{platform="pump-fun"} - mean: solana_platform_volume_24h_usd{platform="pump-fun"} - success: solana_platform_health{platform="pump-fun"} - sample_size: solana_platform_fees_24h_usd{platform="pump-fun"} - series: solana_platform_volume_24h_usd{platform="pump-fun"} - - - slug: gmgn - name: GMGN - tag: "AI trading terminal on Solana" - formula: "24h swap volume in USD attributed to GMGN (Mobula lighthouse byPlatform[name=GMGN].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="gmgn"} - p90: solana_platform_volume_24h_usd{platform="gmgn"} - p99: solana_platform_volume_24h_usd{platform="gmgn"} - mean: solana_platform_volume_24h_usd{platform="gmgn"} - success: solana_platform_health{platform="gmgn"} - sample_size: solana_platform_fees_24h_usd{platform="gmgn"} - series: solana_platform_volume_24h_usd{platform="gmgn"} - - - slug: axiom - name: Axiom - tag: "Web trading terminal on Solana" - formula: "24h swap volume in USD attributed to Axiom (Mobula lighthouse byPlatform[name=Axiom].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="axiom"} - p90: solana_platform_volume_24h_usd{platform="axiom"} - p99: solana_platform_volume_24h_usd{platform="axiom"} - mean: solana_platform_volume_24h_usd{platform="axiom"} - success: solana_platform_health{platform="axiom"} - sample_size: solana_platform_fees_24h_usd{platform="axiom"} - series: solana_platform_volume_24h_usd{platform="axiom"} - - - slug: fomo - name: Fomo - tag: "Solana trading platform" - formula: "24h swap volume in USD attributed to Fomo (Mobula lighthouse byPlatform[name=Fomo].volumeUSD.24h, updated every 15 min). Fees from DeFiLlama (dailyRevenue)." - queries: - p50: solana_platform_volume_24h_usd{platform="fomo"} - p90: solana_platform_volume_24h_usd{platform="fomo"} - p99: solana_platform_volume_24h_usd{platform="fomo"} - mean: solana_platform_volume_24h_usd{platform="fomo"} - success: solana_platform_health{platform="fomo"} - sample_size: defillama_dex_fees_24h_usd{platform="fomo"} - series: solana_platform_volume_24h_usd{platform="fomo"} - - - slug: trojan - name: Trojan - tag: "Telegram trading bot on Solana" - formula: "24h swap volume in USD attributed to Trojan (Mobula lighthouse byPlatform[name=Trojan].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="trojan"} - p90: solana_platform_volume_24h_usd{platform="trojan"} - p99: solana_platform_volume_24h_usd{platform="trojan"} - mean: solana_platform_volume_24h_usd{platform="trojan"} - success: solana_platform_health{platform="trojan"} - sample_size: solana_platform_fees_24h_usd{platform="trojan"} - series: solana_platform_volume_24h_usd{platform="trojan"} - - - slug: padre - name: Padre - tag: "AI Telegram bot on Solana" - formula: "24h swap volume in USD attributed to Padre (Mobula lighthouse byPlatform[name=Padre].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="padre"} - p90: solana_platform_volume_24h_usd{platform="padre"} - p99: solana_platform_volume_24h_usd{platform="padre"} - mean: solana_platform_volume_24h_usd{platform="padre"} - success: solana_platform_health{platform="padre"} - sample_size: solana_platform_fees_24h_usd{platform="padre"} - series: solana_platform_volume_24h_usd{platform="padre"} - - - slug: photon - name: Photon - tag: "Solana trading terminal" - formula: "24h swap volume in USD attributed to Photon (Mobula lighthouse byPlatform[name=Photon].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="photon"} - p90: solana_platform_volume_24h_usd{platform="photon"} - p99: solana_platform_volume_24h_usd{platform="photon"} - mean: solana_platform_volume_24h_usd{platform="photon"} - success: solana_platform_health{platform="photon"} - sample_size: solana_platform_fees_24h_usd{platform="photon"} - series: solana_platform_volume_24h_usd{platform="photon"} - - - slug: maestro - name: Maestro - tag: "Telegram trading bot on Solana" - formula: "24h swap volume in USD attributed to Maestro (Mobula lighthouse byPlatform[name=Maestro].volumeUSD.24h, updated every 15 min)." - queries: - p50: solana_platform_volume_24h_usd{platform="maestro"} - p90: solana_platform_volume_24h_usd{platform="maestro"} - p99: solana_platform_volume_24h_usd{platform="maestro"} - mean: solana_platform_volume_24h_usd{platform="maestro"} - success: solana_platform_health{platform="maestro"} - sample_size: solana_platform_fees_24h_usd{platform="maestro"} - series: solana_platform_volume_24h_usd{platform="maestro"} diff --git a/benchmarks/solana-unique-traders.yml b/benchmarks/solana-unique-traders.yml deleted file mode 100644 index 41e81807..00000000 --- a/benchmarks/solana-unique-traders.yml +++ /dev/null @@ -1,165 +0,0 @@ -# OpenChainBench. Bench 207 - -slug: solana-unique-traders -number: "207" -title: "Unique daily traders on Solana: pump.fun vs GMGN vs Fomo vs Axiom vs Trojan, live" -seo_title: "Unique daily active traders by Solana platform 2026: pump.fun vs GMGN vs Axiom vs Fomo vs Trojan" -seo_description: "{{best_name}} has the most unique daily traders at {{best_p50}} wallets in 24h. pump.fun vs GMGN vs Fomo vs Axiom vs Trojan ranked by unique daily active users, live from Dune Analytics." -subtitle: Live count of unique wallet addresses that executed a trade through each Solana platform in the last 24 hours. Refreshed hourly via Dune Analytics. Measures real user breadth, not volume, revealing which platforms serve the most distinct traders vs which are dominated by bots or whale concentration. - -category: Trading -status: live -metric: Unique traders 24h -unit: count -higher_is_better: true - -disclaimer: | - The metric counts unique transaction IDs (not unique wallet addresses) whose transactions touched a known platform fee wallet in the rolling 24h window. This is a proxy for unique daily active traders; the actual unique-wallet count is typically 3-10x lower since active traders execute multiple trades per day. Counts include bots, MEV searchers, and multi-wallet traders. A transaction that touches two platform fee wallets is counted in both. pump.fun tracking relies on its 9 on-chain fee wallets; since pump.fun cut bonding-curve trading fees to 0% on 2026-08-07, its count reflects only transactions generating graduation and creator fees and substantially understates true trading activity. - -seo_intro: | - GMGN leads Solana trading terminals by unique daily active wallets, serving a broad - segment of retail and power traders with AI-powered copy trading and wallet analysis. - Axiom and Fomo each attract tens of thousands of unique daily wallets, while Trojan - and Maestro serve a smaller but high-conviction base of Telegram power users. - pump.fun's unique trader count is substantially undercounted since August 7, 2026, - when its bonding-curve trading fees were cut to zero; fee wallet attribution now - captures only graduation and creator fee events rather than swap activity. This - benchmark tracks unique daily active wallets per platform live, updated hourly from - Dune Analytics on-chain data, providing a user-breadth view that volume metrics - alone cannot reveal. - -abstract: | - The harness wraps the Dune Analytics API. A single saved query counts distinct - fee_payer addresses in solana.transactions that touched each platform's known fee - wallet addresses in the rolling 24-hour window, joined from solana.account_activity - for efficiency. All platforms including pump.fun use the same fee wallet attribution - approach against solana.account_activity (standard Dune tier). The harness re-executes - the query every hour, polls for completion, then updates the Prometheus gauge - solana_platform_unique_traders_24h per platform. Between executions it serves the - latest cached result every 15 minutes. - -methodology: - - "Source: Dune Analytics (dune.com). Query updated hourly, results fetched every 15 min." - - "All platforms: COUNT(DISTINCT tx_id) from solana.account_activity WHERE address = one of the platform's known fee wallet addresses in the rolling 24h window." - - "Metric represents unique fee-touching transactions per platform, which is a 3-10x upward proxy for unique daily active wallets (power traders execute multiple transactions per day)." - - "pump.fun: tracked via 9 on-chain fee wallets. Since pump.fun cut bonding-curve trading fees to 0% on 2026-08-07, its count primarily reflects graduation and creator fee events, substantially understating true trading activity." - - "Fee wallet sources: DeFiLlama dimension-adapters (master, 2026-08), verified on-chain." - - "Bot inclusion: all transactions counted including automated traders, sniping bots, and copy-trade bots." - - "Double-count across platforms: a transaction that touches two platform fee wallets is counted in both. Counts are not mutually exclusive across platforms." - -findings: - - "{{best_name}} leads Solana platforms with {{best_p50}} unique daily traders." - - "GMGN leads among trading terminals by unique daily wallets, reflecting its broad retail and power-trader user base." - - "Trojan and Maestro serve a smaller but highly active Telegram-native segment, with fewer unique wallets but higher average trade size." - - "Unique traders and volume rankings diverge: Axiom processes more volume than Fomo but has a similar unique-wallet count, suggesting larger average position sizes." - - "pump.fun unique trader count is substantially undercounted post 2026-08-07 due to its zero-fee policy; fee wallet attribution captures only graduation and creator fee events, not swap activity." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-unique-traders - -prometheus: - window: 24h - freshness_metric: solana_platform_unique_traders_24h - -faq: - - q: "What does unique daily traders measure?" - a: "Unique daily traders counts the number of distinct wallet addresses (fee_payer in Solana transactions) that executed at least one trade through a platform in the rolling 24-hour window. It measures user breadth, not volume or frequency. A wallet that trades 1000 times counts as 1 unique trader." - - q: "Why does pump.fun have so many more unique traders than GMGN?" - a: "pump.fun is the default entry point for new Solana users and meme coin traders. Its bonding curve launch mechanic triggers massive multi-wallet bot activity as snipers race to buy newly created tokens. GMGN serves a more deliberate, power-trader segment that skews toward fewer unique wallets but larger position sizes." - - q: "Is this apple-to-apple between pump.fun and the others?" - a: "The methodology is consistent: all platforms use fee wallet attribution from solana.account_activity. However, pump.fun's results are substantially undercounted since August 7, 2026, when its bonding-curve trading fees were cut to zero. Fee wallet attribution now captures only graduation and creator fee events, not the hundreds of thousands of daily swaps. For trading terminals (GMGN, Axiom, Fomo, Trojan, Maestro), the comparison is directly apple-to-apple." - - q: "Why might Fomo's unique trader count be understated?" - a: "Fomo collects protocol fees in USDC via a single fee wallet. Some of Fomo's fee revenue is swept in batches rather than collected per-trade, which may mean some user transactions do not touch the fee wallet directly. The count reflects wallets whose transactions include the fee wallet as an account key." - - q: "How is this different from bench 201 (24h volume)?" - a: "Bench 201 ranks platforms by 24h swap volume in USD. This bench ranks by unique daily active wallets. A platform can have high volume with few wallets (whale-concentrated) or low volume with many wallets (retail-dominated). Both metrics together reveal the full picture." - -providers: - - slug: pump-fun - name: pump.fun - tag: "Solana meme launchpad" - formula: "Unique wallet addresses (distinct fee_payer) whose transaction touched a pump.fun fee wallet in the last 24h (Dune: solana.account_activity, hourly refresh). Substantially undercounted post 2026-08-07 due to zero-fee policy." - queries: - p50: solana_platform_unique_traders_24h{platform="pump-fun"} - p90: solana_platform_unique_traders_24h{platform="pump-fun"} - p99: solana_platform_unique_traders_24h{platform="pump-fun"} - mean: solana_platform_unique_traders_24h{platform="pump-fun"} - success: solana_platform_trader_health{platform="pump-fun"} - sample_size: solana_platform_unique_traders_24h{platform="pump-fun"} - series: solana_platform_unique_traders_24h{platform="pump-fun"} - - - slug: gmgn - name: GMGN - tag: "AI trading terminal on Solana" - formula: "Unique wallet addresses (distinct fee_payer) in transactions touching GMGN fee wallets in the last 24h (Dune: solana.account_activity + solana.transactions, hourly refresh)." - queries: - p50: solana_platform_unique_traders_24h{platform="gmgn"} - p90: solana_platform_unique_traders_24h{platform="gmgn"} - p99: solana_platform_unique_traders_24h{platform="gmgn"} - mean: solana_platform_unique_traders_24h{platform="gmgn"} - success: solana_platform_trader_health{platform="gmgn"} - sample_size: solana_platform_unique_traders_24h{platform="gmgn"} - series: solana_platform_unique_traders_24h{platform="gmgn"} - - - slug: axiom - name: Axiom - tag: "Web trading terminal on Solana" - formula: "Unique wallet addresses in transactions touching Axiom fee wallets in the last 24h (Dune, hourly refresh)." - queries: - p50: solana_platform_unique_traders_24h{platform="axiom"} - p90: solana_platform_unique_traders_24h{platform="axiom"} - p99: solana_platform_unique_traders_24h{platform="axiom"} - mean: solana_platform_unique_traders_24h{platform="axiom"} - success: solana_platform_trader_health{platform="axiom"} - sample_size: solana_platform_unique_traders_24h{platform="axiom"} - series: solana_platform_unique_traders_24h{platform="axiom"} - - - slug: fomo - name: Fomo - tag: "Solana trading platform" - formula: "Unique wallet addresses in transactions touching Fomo USDC fee wallet in the last 24h (Dune, hourly refresh)." - queries: - p50: solana_platform_unique_traders_24h{platform="fomo"} - p90: solana_platform_unique_traders_24h{platform="fomo"} - p99: solana_platform_unique_traders_24h{platform="fomo"} - mean: solana_platform_unique_traders_24h{platform="fomo"} - success: solana_platform_trader_health{platform="fomo"} - sample_size: solana_platform_unique_traders_24h{platform="fomo"} - series: solana_platform_unique_traders_24h{platform="fomo"} - - - slug: trojan - name: Trojan - tag: "Telegram trading bot on Solana" - formula: "Unique wallet addresses in transactions touching Trojan fee wallets in the last 24h (Dune, hourly refresh)." - queries: - p50: solana_platform_unique_traders_24h{platform="trojan"} - p90: solana_platform_unique_traders_24h{platform="trojan"} - p99: solana_platform_unique_traders_24h{platform="trojan"} - mean: solana_platform_unique_traders_24h{platform="trojan"} - success: solana_platform_trader_health{platform="trojan"} - sample_size: solana_platform_unique_traders_24h{platform="trojan"} - series: solana_platform_unique_traders_24h{platform="trojan"} - - - slug: photon - name: Photon - tag: "Solana trading terminal" - formula: "Unique wallet addresses in transactions touching Photon fee wallet in the last 24h (Dune, hourly refresh)." - queries: - p50: solana_platform_unique_traders_24h{platform="photon"} - p90: solana_platform_unique_traders_24h{platform="photon"} - p99: solana_platform_unique_traders_24h{platform="photon"} - mean: solana_platform_unique_traders_24h{platform="photon"} - success: solana_platform_trader_health{platform="photon"} - sample_size: solana_platform_unique_traders_24h{platform="photon"} - series: solana_platform_unique_traders_24h{platform="photon"} - - - slug: maestro - name: Maestro - tag: "Telegram trading bot on Solana" - formula: "Unique wallet addresses in transactions touching Maestro fee wallets in the last 24h (Dune, hourly refresh)." - queries: - p50: solana_platform_unique_traders_24h{platform="maestro"} - p90: solana_platform_unique_traders_24h{platform="maestro"} - p99: solana_platform_unique_traders_24h{platform="maestro"} - mean: solana_platform_unique_traders_24h{platform="maestro"} - success: solana_platform_trader_health{platform="maestro"} - sample_size: solana_platform_unique_traders_24h{platform="maestro"} - series: solana_platform_unique_traders_24h{platform="maestro"} diff --git a/benchmarks/trading-app-execution.yml b/benchmarks/trading-app-execution.yml deleted file mode 100644 index 5394ae19..00000000 --- a/benchmarks/trading-app-execution.yml +++ /dev/null @@ -1,62 +0,0 @@ -# OpenChainBench. Bench 204 - -slug: trading-app-execution -number: "204" -title: "Trading app execution quality: Axiom vs GMGN vs Trojan vs Maestro, Solana + EVM" -seo_title: "Solana trading bot execution quality 2026: Axiom, GMGN, Trojan, Maestro, pump.fun" -seo_description: "Compare on-chain execution quality for Solana trading apps: avg platform fee, priority fee, Jito bundle rate, CU price. Axiom vs GMGN vs Trojan vs Maestro vs pump.fun vs Photon, measured passively from fee accounts, updated hourly." -subtitle: Passive on-chain execution metrics for the top Solana trading apps and Telegram bots. Measured directly from fee-account transactions, no synthetic trades. Priority fee, Jito rate, CU price, and platform fee per transaction. EVM chain revenue (Ethereum, BSC, Base) on the chain tabs. - -category: Trading -status: live -metric: Avg platform fee -unit: sol -higher_is_better: false - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/solana-exec - -abstract: | - The harness monitors the on-chain fee-receiver accounts for each trading app using - the Solana JSON-RPC getSignaturesForAddress method. For every incoming fee transaction, - it records the priority fee, compute units consumed, CU price, presence of a Jito tip, - and the SOL amount transferred to the fee account. Samples are bucketed by hour in - PostgreSQL and materialised every 5 minutes. No trades are simulated; the collector - reads only existing on-chain activity. For EVM chains (Ethereum, BSC, Base), a separate - collector tracks USDC and native ETH/BNB transfers to each platform's fee address using - eth_getLogs on public RPCs; no trace API required. - -methodology: - - "Solana source: Helius RPC getSignaturesForAddress on each platform's fee wallet. 100 transactions sampled per poll (every ~5 min)." - - "Priority fee: transaction fee minus the 5 000-lamport base fee per signature. Paid by the user to compete for block space." - - "CU price: priority fee divided by compute units consumed (microlamports/CU). p50/p95 from sampled CU data." - - "Jito rate: fraction of sampled transactions tipping one of the 8 official Jito tip accounts." - - "Platform fee: average SOL transferred to the platform's fee account per incoming transaction." - - "Tx count: total transactions at the fee account over the window, from raw getSignaturesForAddress counts (not just the 100-tx sample)." - - "EVM source: eth_getLogs for USDC (ERC-20) transfers + native ETH/BNB transfers to each platform's fee address. Base chain: USDC only (no free trace API for native)." - - "EVM cadence: block-by-block scanning, bootstrapped 30 days back on first run, then continuous." - - "All measurements are passive; no test transactions, no funded wallets, no on-chain footprint." - -findings: - - "Axiom leads Solana trading apps by transaction volume, processing over 400K fee transactions per 24h across its 20 fee wallets." - - "GMGN charges the highest average platform fee per transaction among actively-charging platforms, reflecting its higher-notional trader base." - - "Maestro has the highest Jito bundle rate, indicating MEV-sensitive routing for its Telegram bot users." - - "Photon has the highest CU price p95, showing aggressive compute-unit bidding during peak hours." - - "pump.fun fee wallets have received near-zero inflows since August 7 2026 when the bonding-curve was paused; its platform fee row reflects gas only." - -faq: - - q: "What is platform fee vs priority fee?" - a: "Priority fee is paid to Solana validators to compete for block space; it does not go to the trading app. Platform fee is the SOL transferred to the trading app's own fee wallet per transaction, which is the actual revenue the platform captures from each trade." - - q: "Why does Trojan show a near-zero Jito rate?" - a: "Trojan does not appear to use Jito MEV bundles for its standard execution path. This is consistent with its design as a Telegram bot that prioritises simplicity over MEV protection." - - q: "How is transaction count measured?" - a: "Tx count comes from getSignaturesForAddress on the platform's fee wallet(s), which gives the total number of transactions that sent fees to that address. This is the full population count, not a sample; the 100-tx sample is used only for fee amount and CU metrics." - - q: "Why are EVM revenues lower than Solana revenues?" - a: "Most platforms primarily target Solana users. EVM fee collection varies: some platforms charge mostly in native ETH/BNB via internal transactions which are not traceable without a trace API. The EVM figures reflect directly visible ERC-20 (USDC) and native transfers only." - - q: "How often is data updated?" - a: "Solana metrics update every 5 minutes (materialiser cycle). EVM revenue updates block-by-block continuously. The page reflects the state at the last materialiser run, shown in the table header." - - q: "Which platforms are tracked on EVM chains?" - a: "GMGN, Maestro, and Banana Gun are tracked on Ethereum. GMGN, Maestro, Axiom, and Banana Gun are tracked on BSC. GMGN, Maestro, and Banana Gun are tracked on Base. pump.fun Terminal is tracked on all three EVM chains." - -# No prometheus section -- this bench uses a custom page renderer -# at src/app/benchmarks/trading-app-execution/page.tsx that reads -# directly from the exec REST API (exec.openchainbench.com). diff --git a/benchmarks/zetachain-rpc.yml b/benchmarks/zetachain-rpc.yml index 47795c7f..9054761d 100644 --- a/benchmarks/zetachain-rpc.yml +++ b/benchmarks/zetachain-rpc.yml @@ -34,8 +34,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"zetachain\"`. Provider coverage: 1 no-key endpoint (Thirdweb). BlockPI returned 404; AllThatNode DNS is dead." findings: - - "Provider coverage is critically limited: only Thirdweb (`7000.rpc.thirdweb.com`) is configured, and its current success rate from our probes is below 1% — the endpoint responds to one-off requests but blocks sustained per-IP probing. Latency figures reflect the rare successful call and do not represent reliable availability. BlockPI and AllThatNode are dead for ZetaChain; no other free keyless endpoint was found operational at audit time." - - "Manual use of `7000.rpc.thirdweb.com` works for individual calls. The 0.5% probe success rate is an artifact of rate-limiting against our fixed Railway probe IPs, not a general outage." + - "{{best_name}} currently leads free ZetaChain RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free ZetaChain RPC right now?" diff --git a/harnesses/aggregator-head-lag/scrape-cookie b/harnesses/aggregator-head-lag/scrape-cookie deleted file mode 100755 index 585d7baf..00000000 Binary files a/harnesses/aggregator-head-lag/scrape-cookie and /dev/null differ diff --git a/harnesses/aggregator-head-lag/test-utls b/harnesses/aggregator-head-lag/test-utls deleted file mode 100755 index 549c8db9..00000000 Binary files a/harnesses/aggregator-head-lag/test-utls and /dev/null differ diff --git a/harnesses/app-store-ratings/Dockerfile b/harnesses/app-store-ratings/Dockerfile deleted file mode 100644 index 379e2f91..00000000 --- a/harnesses/app-store-ratings/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod ./ -COPY cmd ./cmd -RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o /app/harness ./cmd/script - -FROM debian:bookworm-slim -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/harness /app/harness -EXPOSE 2112 -CMD ["/app/harness"] diff --git a/harnesses/app-store-ratings/cmd/script/itunes.go b/harnesses/app-store-ratings/cmd/script/itunes.go deleted file mode 100644 index b9e70f6a..00000000 --- a/harnesses/app-store-ratings/cmd/script/itunes.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -type itunesEntry struct { - Rating float64 `json:"averageUserRating"` - ReviewCount int `json:"userRatingCount"` - TrackName string `json:"trackName"` -} - -type itunesResp struct { - ResultCount int `json:"resultCount"` - Results []itunesEntry `json:"results"` -} - -func fetchAppStoreData(appID string) (*itunesEntry, error) { - client := &http.Client{Timeout: 10 * time.Second} - url := fmt.Sprintf("https://itunes.apple.com/lookup?id=%s&country=us", appID) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", "OpenChainBench/1.0") - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("fetch: %w", err) - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != 200 { - return nil, fmt.Errorf("status_%d", resp.StatusCode) - } - - var data itunesResp - if err := json.Unmarshal(body, &data); err != nil { - return nil, fmt.Errorf("parse: %w", err) - } - if data.ResultCount == 0 || len(data.Results) == 0 { - return nil, fmt.Errorf("no_results") - } - return &data.Results[0], nil -} diff --git a/harnesses/app-store-ratings/cmd/script/main.go b/harnesses/app-store-ratings/cmd/script/main.go deleted file mode 100644 index 8cadd48a..00000000 --- a/harnesses/app-store-ratings/cmd/script/main.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "os" - "time" - - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var apps = []struct { - slug string - id string -}{ - {"pump-fun", "6717572591"}, - {"fomo", "6741115427"}, - {"gmgn", "6745328711"}, - {"moonshot", "6503993131"}, - {"coinbase", "886427730"}, - {"bybit", "1488296980"}, - {"kraken", "1481947260"}, - {"binance-us", "1492670702"}, - {"robinhood", "938003185"}, - {"cryptocom", "1262148500"}, - {"invo", "1601301148"}, -} - -func main() { - initMetrics() - - if err := runOnce(); err != nil { - fmt.Fprintf(os.Stderr, "[app-store] first run: %v\n", err) - } - - go func() { - tick := time.NewTicker(30 * time.Minute) - for range tick.C { - if err := runOnce(); err != nil { - fmt.Fprintf(os.Stderr, "[app-store] poll: %v\n", err) - } - } - }() - - http.Handle("/metrics", promhttp.Handler()) - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) - fmt.Println("[app-store-ratings] metrics on :2112") - if err := http.ListenAndServe(":2112", nil); err != nil { - fmt.Fprintf(os.Stderr, "server: %v\n", err) - os.Exit(1) - } -} - -func runOnce() error { - for _, app := range apps { - data, err := fetchAppStoreData(app.id) - if err != nil { - fmt.Printf("[app-store] %s: %v\n", app.slug, err) - appStoreHealth.WithLabelValues(app.slug).Set(0) - continue - } - appStoreRating.WithLabelValues(app.slug).Set(data.Rating) - appStoreReviews.WithLabelValues(app.slug).Set(float64(data.ReviewCount)) - appStoreHealth.WithLabelValues(app.slug).Set(1) - fmt.Printf("[app-store] %s: rating=%.4f reviews=%d\n", app.slug, data.Rating, data.ReviewCount) - } - return nil -} diff --git a/harnesses/app-store-ratings/cmd/script/metrics.go b/harnesses/app-store-ratings/cmd/script/metrics.go deleted file mode 100644 index 4d1aa5e6..00000000 --- a/harnesses/app-store-ratings/cmd/script/metrics.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - appStoreRating = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "app_store_rating", - Help: "Apple App Store average user rating (0 to 5).", - }, []string{"app"}) - - appStoreReviews = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "app_store_reviews_total", - Help: "Total number of user ratings in the Apple App Store.", - }, []string{"app"}) - - appStoreHealth = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "app_store_health", - Help: "1 if the iTunes lookup succeeded in the last poll.", - }, []string{"app"}) -) - -func initMetrics() {} diff --git a/harnesses/app-store-ratings/go.mod b/harnesses/app-store-ratings/go.mod deleted file mode 100644 index af2f9da2..00000000 --- a/harnesses/app-store-ratings/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module app-store-ratings - -go 1.24.0 - -require github.com/prometheus/client_golang v1.23.2 diff --git a/harnesses/bridge-monitor/cmd/monitor/squid_bridge.go b/harnesses/bridge-monitor/cmd/monitor/squid_bridge.go index e5772f69..afc962b8 100644 --- a/harnesses/bridge-monitor/cmd/monitor/squid_bridge.go +++ b/harnesses/bridge-monitor/cmd/monitor/squid_bridge.go @@ -169,16 +169,7 @@ func (s *SquidBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawU gasUsd += v } - // Prefer fromAmountUSD - toAmountUSD as the all-in cost (captures spread + fees + gas - // as netted by the router). Fall back to explicit fee+gas sum when the USD amounts - // are identical (e.g. stable→stable where spot prices cancel out). - diffUsd := inUsd - outUsd - var costUsd float64 - if diffUsd > 0.001 { - costUsd = diffUsd - } else { - costUsd = bridgeFeeUsd + gasUsd - } + costUsd := inUsd - outUsd + gasUsd if costUsd < 0 { costUsd = 0 } @@ -186,7 +177,7 @@ func (s *SquidBridge) TestRoute(route TestRoute, amount, amountUsd float64, rawU if amountUsd > 0 { costPct = (costUsd / amountUsd) * 100 } - slippage := diffUsd - bridgeFeeUsd - gasUsd + slippage := costUsd - bridgeFeeUsd - gasUsd if slippage < 0 { slippage = 0 } diff --git a/harnesses/evm-exec/Dockerfile b/harnesses/evm-exec/Dockerfile deleted file mode 100644 index 4058acc2..00000000 --- a/harnesses/evm-exec/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM golang:1.24-alpine AS builder -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 go build -o /app/collector ./cmd/collector -RUN CGO_ENABLED=0 go build -o /app/materializer ./cmd/materializer - -FROM alpine:3.20 -RUN apk add --no-cache ca-certificates tzdata -COPY --from=builder /app/collector /app/collector -COPY --from=builder /app/materializer /app/materializer -COPY migrations/ /app/migrations/ diff --git a/harnesses/evm-exec/cmd/collector/main.go b/harnesses/evm-exec/cmd/collector/main.go deleted file mode 100644 index b91fc079..00000000 --- a/harnesses/evm-exec/cmd/collector/main.go +++ /dev/null @@ -1,377 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "time" - - "github.com/ChainBench/OpenChainBench/harnesses/evm-exec/internal/platform" - "github.com/ChainBench/OpenChainBench/harnesses/evm-exec/internal/source" - "github.com/ChainBench/OpenChainBench/harnesses/evm-exec/internal/store" -) - -var chainRPC map[string]string - -func main() { - chainRPC = map[string]string{ - "ethereum": envOrDefault("ETH_RPC_URL", "https://eth.drpc.org"), - "bsc": bscRPC(), - "base": envOrDefault("BASE_RPC_URL", "https://base.drpc.org"), - "robinhood": envOrDefault("RH_RPC_URL", "https://rpc.mainnet.chain.robinhood.com"), - } - - ctx := context.Background() - db, err := store.New(ctx, mustEnv("DATABASE_URL")) - if err != nil { - log.Fatalf("collector: %v", err) - } - defer db.Close() - - etherscanKey := mustEnv("ETHERSCAN_API_KEY") - - if err := bootstrap(ctx, db); err != nil { - log.Fatalf("collector: bootstrap: %v", err) - } - log.Printf("collector: ready") - - for { - for plt, chains := range platform.PlatformConfig { - for chain, cfg := range chains { - if err := collectPlatform(ctx, db, etherscanKey, plt, chain, cfg); err != nil { - log.Printf("collector: %s/%s: %v", plt, chain, err) - } - } - } - time.Sleep(10 * time.Minute) - } -} - -// bootstrap seeds cursors at (head - 30d) for each (platform, chain, asset) that has no cursor. -func bootstrap(ctx context.Context, db *store.DB) error { - for plt, chains := range platform.PlatformConfig { - for chain, cfg := range chains { - rpc := chainRPC[chain] - head, err := source.HeadBlock(ctx, rpc) - if err != nil { - return err - } - bpd := platform.BlocksPerDay[chain] - start := uint64(0) - if head > bpd*uint64(cfg.BootstrapDays) { - start = head - bpd*uint64(cfg.BootstrapDays) - } - - for _, token := range cfg.ERC20Tokens { - if err := db.SeedCursorIfAbsent(ctx, chain, plt, token, start); err != nil { - return err - } - } - if cfg.NativeEnabled { - if err := db.SeedCursorIfAbsent(ctx, chain, plt, "native", start); err != nil { - return err - } - } - log.Printf("collector: bootstrap %s/%s start=%d", plt, chain, start) - } - } - return nil -} - -func collectPlatform(ctx context.Context, db *store.DB, etherscanKey, plt, chain string, cfg platform.EVMPlatform) error { - rpc := chainRPC[chain] - toBlock, err := finalityBlock(ctx, rpc, chain) - if err != nil { - return err - } - - for _, token := range cfg.ERC20Tokens { - if err := collectERC20(ctx, db, rpc, plt, chain, cfg.FeeCollector, token, toBlock); err != nil { - log.Printf("collector: %s/%s ERC20 %s...: %v", plt, chain, token[:8], err) - } - } - - if cfg.NativeEnabled { - switch chain { - case "ethereum": - if err := collectNativeEtherscan(ctx, db, etherscanKey, source.ChainIDEthereum, plt, chain, cfg.FeeCollector, toBlock); err != nil { - log.Printf("collector: %s/%s native ETH: %v", plt, chain, err) - } - case "base": - if err := collectNativeEtherscan(ctx, db, etherscanKey, source.ChainIDBase, plt, chain, cfg.FeeCollector, toBlock); err != nil { - log.Printf("collector: %s/%s native ETH on Base: %v", plt, chain, err) - } - case "bsc": - if err := collectNativeBSC(ctx, db, plt, chain, cfg.FeeCollector, toBlock); err != nil { - log.Printf("collector: %s/%s native BNB: %v", plt, chain, err) - } - case "robinhood": - if err := collectNativeBlockscout(ctx, db, plt, chain, cfg.FeeCollector, toBlock); err != nil { - log.Printf("collector: %s/%s native ETH (Blockscout): %v", plt, chain, err) - } - } - } - return nil -} - -func collectERC20(ctx context.Context, db *store.DB, rpc, plt, chain, collector, token string, toBlock uint64) error { - cursor, _, err := db.GetCursor(ctx, chain, plt, token) - if err != nil { - return fmt.Errorf("get cursor: %w", err) - } - if cursor >= toBlock { - return nil - } - decimals := platform.TokenDecimals[token] - - transfers, lastBlock, err := source.GetERC20TransfersAdaptive(ctx, rpc, token, collector, cursor+1, toBlock) - if err != nil { - return err - } - - events := make([]store.EVMEvent, 0, len(transfers)) - for _, t := range transfers { - var bt time.Time - if !t.BlockTime.IsZero() { - bt = t.BlockTime - _ = db.CacheBlockTime(ctx, chain, t.BlockNum, bt) - } else { - var err error - bt, err = resolveBlockTime(ctx, db, rpc, chain, t.BlockNum) - if err != nil { - continue - } - } - events = append(events, store.EVMEvent{ - Chain: chain, TxHash: t.TxHash, BlockNum: t.BlockNum, BlockTime: bt, - Platform: plt, Asset: token, AmountRaw: t.Amount, - Decimals: decimals, EventKey: source.LogIndexKey(t.LogIndex), - }) - } - if err := db.UpsertEvents(ctx, events); err != nil { - return err - } - if lastBlock > cursor { - _ = db.SaveCursor(ctx, chain, plt, token, lastBlock) - } - if len(events) > 0 { - log.Printf("collector: %s/%s ERC20 %d events block=%d", plt, chain, len(events), lastBlock) - } - return nil -} - -func collectNativeEtherscan(ctx context.Context, db *store.DB, apiKey, chainID, plt, chain, collector string, toBlock uint64) error { - cursor, _, err := db.GetCursor(ctx, chain, plt, "native") - if err != nil { - return fmt.Errorf("get cursor: %w", err) - } - - internalTxs, lastInt, err := source.GetEtherscanInternalTxs(ctx, apiKey, chainID, collector, cursor) - if err != nil { - return err - } - normalTxs, lastNorm, err := source.GetEtherscanNormalTxs(ctx, apiKey, chainID, collector, cursor) - if err != nil { - return err - } - - rpc := chainRPC[chain] - var events []store.EVMEvent - for _, tx := range append(internalTxs, normalTxs...) { - if tx.BlockNum > toBlock { - continue - } - var bt time.Time - if !tx.BlockTime.IsZero() { - bt = tx.BlockTime - _ = db.CacheBlockTime(ctx, chain, tx.BlockNum, bt) - } else { - var err error - bt, err = resolveBlockTime(ctx, db, rpc, chain, tx.BlockNum) - if err != nil { - continue - } - } - events = append(events, store.EVMEvent{ - Chain: chain, TxHash: tx.TxHash, BlockNum: tx.BlockNum, BlockTime: bt, - Platform: plt, Asset: "native", AmountRaw: tx.Amount, - Decimals: 18, EventKey: tx.EventKey, - }) - } - if err := db.UpsertEvents(ctx, events); err != nil { - return err - } - if high := max64(lastInt, lastNorm); high > cursor { - _ = db.SaveCursor(ctx, chain, plt, "native", high) - } - if len(events) > 0 { - log.Printf("collector: %s/%s native ETH %d events", plt, chain, len(events)) - } - return nil -} - -func collectNativeBlockscout(ctx context.Context, db *store.DB, plt, chain, collector string, toBlock uint64) error { - cursor, _, err := db.GetCursor(ctx, chain, plt, "native") - if err != nil { - return fmt.Errorf("get cursor: %w", err) - } - if cursor >= toBlock { - return nil - } - rpc := chainRPC[chain] - - internalTxs, lastInt, err := source.GetBlockscoutInternalTxs(ctx, source.BlockscoutRobinhood, collector, cursor) - if err != nil { - return err - } - normalTxs, lastNorm, err := source.GetBlockscoutNormalTxs(ctx, source.BlockscoutRobinhood, collector, cursor) - if err != nil { - return err - } - - var events []store.EVMEvent - for _, tx := range append(internalTxs, normalTxs...) { - if tx.BlockNum > toBlock { - continue - } - var bt time.Time - if !tx.BlockTime.IsZero() { - bt = tx.BlockTime - _ = db.CacheBlockTime(ctx, chain, tx.BlockNum, bt) - } else { - bt, err = resolveBlockTime(ctx, db, rpc, chain, tx.BlockNum) - if err != nil { - continue - } - } - events = append(events, store.EVMEvent{ - Chain: chain, TxHash: tx.TxHash, BlockNum: tx.BlockNum, BlockTime: bt, - Platform: plt, Asset: "native", AmountRaw: tx.Amount, - Decimals: 18, EventKey: tx.EventKey, - }) - } - if err := db.UpsertEvents(ctx, events); err != nil { - return err - } - if high := max64(lastInt, lastNorm); high > cursor { - _ = db.SaveCursor(ctx, chain, plt, "native", high) - } - if len(events) > 0 { - log.Printf("collector: %s/%s native ETH (Blockscout) %d events block=%d", plt, chain, len(events), max64(lastInt, lastNorm)) - } - return nil -} - -func collectNativeBSC(ctx context.Context, db *store.DB, plt, chain, collector string, toBlock uint64) error { - cursor, _, err := db.GetCursor(ctx, chain, plt, "native") - if err != nil { - return fmt.Errorf("get cursor: %w", err) - } - if cursor >= toBlock { - return nil - } - rpc := chainRPC[chain] - - transfers, err := source.GetNativeTransfers(ctx, rpc, collector, cursor+1, toBlock) - if err != nil { - return err - } - - var events []store.EVMEvent - var highBlock uint64 - for _, t := range transfers { - var bt time.Time - if !t.BlockTime.IsZero() { - bt = t.BlockTime - _ = db.CacheBlockTime(ctx, chain, t.BlockNum, bt) - } else { - var err error - bt, err = resolveBlockTime(ctx, db, rpc, chain, t.BlockNum) - if err != nil { - continue - } - } - events = append(events, store.EVMEvent{ - Chain: chain, TxHash: t.TxHash, BlockNum: t.BlockNum, BlockTime: bt, - Platform: plt, Asset: "native", AmountRaw: t.Amount, - Decimals: 18, EventKey: t.EventKey, - }) - if t.BlockNum > highBlock { - highBlock = t.BlockNum - } - } - if err := db.UpsertEvents(ctx, events); err != nil { - return err - } - if highBlock > cursor { - _ = db.SaveCursor(ctx, chain, plt, "native", highBlock) - } - if len(events) > 0 { - log.Printf("collector: %s/%s native BNB %d events block=%d", plt, chain, len(events), highBlock) - } - return nil -} - -func resolveBlockTime(ctx context.Context, db *store.DB, rpc, chain string, blockNum uint64) (time.Time, error) { - if t, ok := db.GetBlockTime(ctx, chain, blockNum); ok { - return t, nil - } - t, err := source.BlockTime(ctx, rpc, blockNum) - if err != nil { - return time.Time{}, err - } - _ = db.CacheBlockTime(ctx, chain, blockNum, t) - return t, nil -} - -func finalityBlock(ctx context.Context, rpc, chain string) (uint64, error) { - switch chain { - case "ethereum", "base": - return source.ResolveBlockTag(ctx, rpc, "finalized") - case "bsc": - head, err := source.HeadBlock(ctx, rpc) - if err != nil { - return 0, err - } - if head > 20 { - return head - 20, nil - } - return 0, nil - default: - return source.HeadBlock(ctx, rpc) - } -} - -func bscRPC() string { - if v := os.Getenv("BSC_RPC_URL"); v != "" { - return v - } - key := os.Getenv("NODEREAL_BSC_KEY") - if key == "" { - log.Fatal("collector: NODEREAL_BSC_KEY or BSC_RPC_URL is required") - } - return "https://bsc-mainnet.nodereal.io/v1/" + key -} - -func envOrDefault(key, def string) string { - if v := os.Getenv(key); v != "" { - return v - } - return def -} - -func mustEnv(key string) string { - v := os.Getenv(key) - if v == "" { - log.Fatalf("missing env: %s", key) - } - return v -} - -func max64(a, b uint64) uint64 { - if a > b { - return a - } - return b -} diff --git a/harnesses/evm-exec/cmd/materializer/main.go b/harnesses/evm-exec/cmd/materializer/main.go deleted file mode 100644 index ece79c69..00000000 --- a/harnesses/evm-exec/cmd/materializer/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - "time" - - "github.com/ChainBench/OpenChainBench/harnesses/evm-exec/internal/store" -) - -func main() { - ctx := context.Background() - db, err := store.New(ctx, mustEnv("DATABASE_URL")) - if err != nil { - log.Fatalf("materializer: %v", err) - } - defer db.Close() - - for { - if err := db.Materialize(ctx); err != nil { - log.Printf("materializer: %v", err) - } else { - log.Printf("materializer: done") - } - time.Sleep(5 * time.Minute) - } -} - -func mustEnv(key string) string { - v := os.Getenv(key) - if v == "" { - log.Fatalf("missing env: %s", key) - } - return v -} diff --git a/harnesses/evm-exec/docker-compose.evm-exec.yml b/harnesses/evm-exec/docker-compose.evm-exec.yml deleted file mode 100644 index de33a5f0..00000000 --- a/harnesses/evm-exec/docker-compose.evm-exec.yml +++ /dev/null @@ -1,36 +0,0 @@ -# Fragment to merge into /opt/ocb/docker-compose.yml on VPS. -# Requires: ocb-postgres (from docker-compose.apps.yml). -# Env: /run/ocb/.env.evm-exec -# DATABASE_URL, ETHERSCAN_API_KEY, NODEREAL_BSC_KEY -# Optional: ETH_RPC_URL (default https://eth.drpc.org) -# BASE_RPC_URL (default https://base.drpc.org) -# BSC_RPC_URL (overrides NODEREAL_BSC_KEY) - -services: - evm-exec-collector: - build: /opt/ocb/harnesses/evm-exec - container_name: ocb-evm-exec-collector - restart: unless-stopped - env_file: /run/ocb/.env.evm-exec - command: ["/app/collector"] - networks: [web] - depends_on: - postgres: - condition: service_healthy - logging: - driver: json-file - options: { max-size: "20m", max-file: "5" } - - evm-exec-materializer: - build: /opt/ocb/harnesses/evm-exec - container_name: ocb-evm-exec-materializer - restart: unless-stopped - env_file: /run/ocb/.env.evm-exec - command: ["/app/materializer"] - networks: [web] - depends_on: - postgres: - condition: service_healthy - logging: - driver: json-file - options: { max-size: "10m", max-file: "3" } diff --git a/harnesses/evm-exec/go.mod b/harnesses/evm-exec/go.mod deleted file mode 100644 index 437438a5..00000000 --- a/harnesses/evm-exec/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module github.com/ChainBench/OpenChainBench/harnesses/evm-exec - -go 1.24.0 - -require github.com/jackc/pgx/v5 v5.7.2 - -require ( - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect -) diff --git a/harnesses/evm-exec/go.sum b/harnesses/evm-exec/go.sum deleted file mode 100644 index 731b5dfd..00000000 --- a/harnesses/evm-exec/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/evm-exec/internal/platform/platforms.go b/harnesses/evm-exec/internal/platform/platforms.go deleted file mode 100644 index 098100b9..00000000 --- a/harnesses/evm-exec/internal/platform/platforms.go +++ /dev/null @@ -1,199 +0,0 @@ -package platform - -import ( - "strings" -) - -// EVMPlatform describes what to monitor for one platform on one chain. -type EVMPlatform struct { - FeeCollector string // lowercase 42-char 0x address - NativeEnabled bool // collect native asset (ETH / BNB) - ERC20Tokens []string // lowercase ERC-20 contracts to monitor - BootstrapDays int // how many days back to seed cursor on first run -} - -// USDC contract addresses, lowercase. -const ( - USDC_ETH = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" // 6 decimals - USDC_BSC = "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d" // 18 decimals (Binance-Peg) - USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" // 6 decimals - USDG_ROBINHOOD = "0x5fc5360d0400a0fd4f2af552add042d716f1d168" // 6 decimals, Robinhood Chain stablecoin -) - -// TokenDecimals maps known token addresses to their decimal places. -var TokenDecimals = map[string]int{ - USDC_ETH: 6, - USDC_BSC: 18, - USDC_BASE: 6, - USDG_ROBINHOOD: 6, -} - -// NativeSymbol returns the native asset symbol for a chain. -func NativeSymbol(chain string) string { - if chain == "bsc" { - return "BNB" - } - return "ETH" -} - -// BlocksPerDay is the approximate number of blocks produced per day per chain. -var BlocksPerDay = map[string]uint64{ - "ethereum": 7_200, - "bsc": 28_800, - "base": 43_200, - "robinhood": 864_000, // ~10 blocks/second (Arbitrum Orbit) -} - -// PlatformConfig maps platform → chain → configuration. -var PlatformConfig = map[string]map[string]EVMPlatform{ - // pump.fun Terminal (acquired Terminal.fun/Padre Oct 2025) — confirmed via DeFiLlama - "pumpfun": { - "ethereum": { - FeeCollector: "0xa74fa823bc8617fa320a966b3d11b0f722ef09ee", - NativeEnabled: true, - ERC20Tokens: []string{USDC_ETH}, - BootstrapDays: 30, - }, - "bsc": { - FeeCollector: "0x2b0a28a0a9197f8af5d1b8371c048e92dd78b640", - NativeEnabled: true, - ERC20Tokens: []string{USDC_BSC}, - BootstrapDays: 30, - }, - "base": { - FeeCollector: "0x16388de42c5829fd0e88c8eb001ef43bfc93f177", - NativeEnabled: true, // Etherscan V2 chainid=8453, same ETHERSCAN_API_KEY - ERC20Tokens: []string{USDC_BASE}, - BootstrapDays: 30, - }, - }, - // Maestro — confirmed via DeFiLlama fees/maestro adapter, same address all EVM chains - "maestro": { - "ethereum": { - FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", - NativeEnabled: true, - ERC20Tokens: []string{USDC_ETH}, - BootstrapDays: 30, - }, - "bsc": { - FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", - NativeEnabled: true, - ERC20Tokens: []string{USDC_BSC}, - BootstrapDays: 30, - }, - "base": { - FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", - NativeEnabled: true, // Etherscan V2 chainid=8453 - ERC20Tokens: []string{USDC_BASE}, - BootstrapDays: 30, - }, - }, - // Banana Gun — these addresses are routers, not pure fee wallets. - // Native disabled on all chains: internal ETH/BNB = trade principal, not fees. - // USDC inflows are also router throughput; evmKey=null in frontend so nothing is displayed. - // Proper fix: eth_getLogs on topic 0x72015ace… for actual fee events. - "banana-gun": { - "ethereum": { - FeeCollector: "0x3328f7f4a1d1c57c35df56bbf0c9dcafca309c49", - NativeEnabled: false, - ERC20Tokens: []string{USDC_ETH}, - BootstrapDays: 30, - }, - "bsc": { - FeeCollector: "0x461efe0100be0682545972ebfc8b4a13253bd602", - NativeEnabled: false, - ERC20Tokens: []string{USDC_BSC}, - BootstrapDays: 30, - }, - "base": { - FeeCollector: "0x1fba6b0bbae2b74586fba407fb45bd4788b7b130", - NativeEnabled: false, // was incorrectly true; same router logic applies on Base - ERC20Tokens: []string{USDC_BASE}, - BootstrapDays: 30, - }, - }, - "gmgn": { - "ethereum": { - FeeCollector: "0xb8159ba378904f803639d274cec79f788931c9c8", - NativeEnabled: true, - ERC20Tokens: []string{USDC_ETH}, - BootstrapDays: 30, - }, - "bsc": { - FeeCollector: "0xb8159ba378904f803639d274cec79f788931c9c8", - NativeEnabled: true, - ERC20Tokens: []string{USDC_BSC}, - BootstrapDays: 30, - }, - "base": { - FeeCollector: "0xb8159ba378904f803639d274cec79f788931c9c8", - NativeEnabled: true, // Etherscan V2 chainid=8453 - ERC20Tokens: []string{USDC_BASE}, - BootstrapDays: 30, - }, - }, - // Axiom — confirmed via DeFiLlama fees/axiom adapter (feeReceiver field, start 2026-01-25) - "axiom": { - "bsc": { - FeeCollector: "0xdec29d79e8cdf009d2fa33e0558cb5648481cac3", - NativeEnabled: true, - ERC20Tokens: []string{USDC_BSC}, - BootstrapDays: 30, - }, - }, - // GMGN + Maestro on Robinhood Chain (Arbitrum Orbit L2, chainId=4663) - // Same fee-collector addresses as ETH/BSC; stablecoin is USDG (Robinhood's native stable). - // Native ETH collected via Blockscout (robinhoodchain.blockscout.com/api, txlistinternal). - "gmgn-robinhood": { - "robinhood": { - FeeCollector: "0xb8159ba378904f803639d274cec79f788931c9c8", - NativeEnabled: true, - ERC20Tokens: []string{USDG_ROBINHOOD}, - BootstrapDays: 30, - }, - }, - "maestro-robinhood": { - "robinhood": { - FeeCollector: "0xb0999731f7c2581844658a9d2ced1be0077b7397", - NativeEnabled: true, - ERC20Tokens: []string{USDG_ROBINHOOD}, - BootstrapDays: 30, - }, - }, -} - -// Coverage returns "full" if native is enabled for this platform+chain, else "stable-only". -func Coverage(platform, chain string) string { - chains, ok := PlatformConfig[platform] - if !ok { - return "stable-only" - } - cfg, ok := chains[chain] - if !ok { - return "stable-only" - } - if cfg.NativeEnabled { - return "full" - } - return "stable-only" -} - -func init() { - for plt, chains := range PlatformConfig { - for chain, cfg := range chains { - mustAddr(cfg.FeeCollector, plt+"/"+chain+"/FeeCollector") - for _, t := range cfg.ERC20Tokens { - mustAddr(t, plt+"/"+chain+"/ERC20Token") - } - } - } - for addr := range TokenDecimals { - mustAddr(addr, "TokenDecimals key") - } -} - -func mustAddr(a, label string) { - if len(a) != 42 || !strings.HasPrefix(a, "0x") || a != strings.ToLower(a) { - panic("invalid address in platform config [" + label + "]: " + a) - } -} diff --git a/harnesses/evm-exec/internal/source/blockscout.go b/harnesses/evm-exec/internal/source/blockscout.go deleted file mode 100644 index 06ca775f..00000000 --- a/harnesses/evm-exec/internal/source/blockscout.go +++ /dev/null @@ -1,289 +0,0 @@ -package source - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "net/http" - "net/url" - "strings" - "time" -) - -// Blockscout Etherscan-compatible API for chains that have a Blockscout explorer -// but are not supported by Etherscan V2. -const ( - BlockscoutRobinhood = "https://robinhoodchain.blockscout.com/api" -) - -// blockscoutTx matches the Blockscout txlistinternal result shape. -type blockscoutTx struct { - TransactionHash string `json:"transactionHash"` - BlockNumber string `json:"blockNumber"` - TimeStamp string `json:"timeStamp"` - Value string `json:"value"` - To string `json:"to"` - IsError string `json:"isError"` - Index string `json:"index"` // trace position within the tx (event key) -} - -// blockscoutNormalTx matches the Blockscout txlist (normal/external tx) result shape. -// Field names differ from txlistinternal: hash vs transactionHash, no index. -type blockscoutNormalTx struct { - Hash string `json:"hash"` - BlockNumber string `json:"blockNumber"` - TimeStamp string `json:"timeStamp"` - Value string `json:"value"` - To string `json:"to"` - IsError string `json:"isError"` -} - -// GetBlockscoutInternalTxs fetches internal ETH transfers to address via Blockscout. -// apiURL is the chain-specific Blockscout API base (e.g. blockscoutRobinhood). -func GetBlockscoutInternalTxs(ctx context.Context, apiURL, address string, startBlock uint64) ([]NativeTx, uint64, error) { - const offset = 1000 - const maxPage = 20 // 20k results max before we advance the window - - addrLower := strings.ToLower(address) - var all []NativeTx - highestBlock := startBlock - curStart := startBlock - - for { - var windowTxs []NativeTx - hitCap := false - - for page := 1; page <= maxPage; page++ { - time.Sleep(300 * time.Millisecond) - - params := url.Values{ - "module": {"account"}, - "action": {"txlistinternal"}, - "address": {address}, - "startblock": {fmt.Sprintf("%d", curStart)}, - "endblock": {"99999999"}, - "page": {fmt.Sprintf("%d", page)}, - "offset": {fmt.Sprintf("%d", offset)}, - "sort": {"asc"}, - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL+"?"+params.Encode(), nil) - if err != nil { - return all, highestBlock, err - } - resp, err := httpClient.Do(req) - if err != nil { - return all, highestBlock, fmt.Errorf("blockscout page %d: %w", page, err) - } - - var out struct { - Status string `json:"status"` - Message string `json:"message"` - Result json.RawMessage `json:"result"` - } - decErr := json.NewDecoder(resp.Body).Decode(&out) - resp.Body.Close() - if decErr != nil { - return all, highestBlock, fmt.Errorf("blockscout decode: %w", decErr) - } - - // Blockscout status "0" = error/empty, "1" = full results, "2" = partial (still valid). - if out.Status == "0" { - break - } - if out.Status != "1" && out.Status != "2" { - var msg string - json.Unmarshal(out.Result, &msg) //nolint:errcheck - return all, highestBlock, fmt.Errorf("blockscout: %s %s", out.Message, msg) - } - - var txs []blockscoutTx - if err := json.Unmarshal(out.Result, &txs); err != nil { - break - } - - for _, tx := range txs { - if tx.IsError == "1" || tx.Value == "" || tx.Value == "0" { - continue - } - if strings.ToLower(tx.To) != addrLower { - continue - } - amt, err := new(big.Int).SetString(tx.Value, 10) - if !err || amt.Sign() <= 0 { - continue - } - blockNum, parseErr := parseDecU64(tx.BlockNumber) - if parseErr != nil { - continue - } - var bt time.Time - if ts, tsErr := parseDecU64(tx.TimeStamp); tsErr == nil && ts > 0 { - bt = time.Unix(int64(ts), 0).UTC() - } - ntx := NativeTx{ - TxHash: strings.ToLower(tx.TransactionHash), - BlockNum: blockNum, - BlockTime: bt, - Amount: amt, - EventKey: tx.Index, - } - windowTxs = append(windowTxs, ntx) - if blockNum > highestBlock { - highestBlock = blockNum - } - } - - if len(txs) < offset { - break - } - if page == maxPage { - hitCap = true - break - } - } - - all = append(all, windowTxs...) - - if !hitCap || len(windowTxs) == 0 { - break - } - - var lastBlock uint64 - for _, tx := range windowTxs { - if tx.BlockNum > lastBlock { - lastBlock = tx.BlockNum - } - } - if lastBlock <= curStart { - break - } - curStart = lastBlock - } - - return all, highestBlock, nil -} - -// GetBlockscoutNormalTxs fetches direct ETH transfers (tx.value > 0, to == address) via txlist. -// Complements GetBlockscoutInternalTxs: direct EOA sends don't appear in txlistinternal. -func GetBlockscoutNormalTxs(ctx context.Context, apiURL, address string, startBlock uint64) ([]NativeTx, uint64, error) { - const offset = 1000 - const maxPage = 20 - - addrLower := strings.ToLower(address) - var all []NativeTx - highestBlock := startBlock - curStart := startBlock - - for { - var windowTxs []NativeTx - hitCap := false - - for page := 1; page <= maxPage; page++ { - time.Sleep(300 * time.Millisecond) - - params := url.Values{ - "module": {"account"}, - "action": {"txlist"}, - "address": {address}, - "startblock": {fmt.Sprintf("%d", curStart)}, - "endblock": {"99999999"}, - "page": {fmt.Sprintf("%d", page)}, - "offset": {fmt.Sprintf("%d", offset)}, - "sort": {"asc"}, - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL+"?"+params.Encode(), nil) - if err != nil { - return all, highestBlock, err - } - resp, err := httpClient.Do(req) - if err != nil { - return all, highestBlock, fmt.Errorf("blockscout txlist page %d: %w", page, err) - } - - var out struct { - Status string `json:"status"` - Message string `json:"message"` - Result json.RawMessage `json:"result"` - } - decErr := json.NewDecoder(resp.Body).Decode(&out) - resp.Body.Close() - if decErr != nil { - return all, highestBlock, fmt.Errorf("blockscout txlist decode: %w", decErr) - } - - if out.Status == "0" { - break - } - if out.Status != "1" && out.Status != "2" { - break - } - - var txs []blockscoutNormalTx - if err := json.Unmarshal(out.Result, &txs); err != nil { - break - } - - for _, tx := range txs { - if tx.IsError == "1" || tx.Value == "" || tx.Value == "0" { - continue - } - if strings.ToLower(tx.To) != addrLower { - continue - } - amt, ok := new(big.Int).SetString(tx.Value, 10) - if !ok || amt.Sign() <= 0 { - continue - } - blockNum, parseErr := parseDecU64(tx.BlockNumber) - if parseErr != nil { - continue - } - var bt time.Time - if ts, tsErr := parseDecU64(tx.TimeStamp); tsErr == nil && ts > 0 { - bt = time.Unix(int64(ts), 0).UTC() - } - ntx := NativeTx{ - TxHash: strings.ToLower(tx.Hash), - BlockNum: blockNum, - BlockTime: bt, - Amount: amt, - EventKey: "top", // direct tx value, not a trace - } - windowTxs = append(windowTxs, ntx) - if blockNum > highestBlock { - highestBlock = blockNum - } - } - - if len(txs) < offset { - break - } - if page == maxPage { - hitCap = true - break - } - } - - all = append(all, windowTxs...) - - if !hitCap || len(windowTxs) == 0 { - break - } - - var lastBlock uint64 - for _, tx := range windowTxs { - if tx.BlockNum > lastBlock { - lastBlock = tx.BlockNum - } - } - if lastBlock <= curStart { - break - } - curStart = lastBlock - } - - return all, highestBlock, nil -} diff --git a/harnesses/evm-exec/internal/source/blocktime.go b/harnesses/evm-exec/internal/source/blocktime.go deleted file mode 100644 index 83386ed5..00000000 --- a/harnesses/evm-exec/internal/source/blocktime.go +++ /dev/null @@ -1,110 +0,0 @@ -package source - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - "time" -) - -var httpClient = &http.Client{Timeout: 20 * time.Second} - -// HeadBlock returns the current head block number via eth_blockNumber. -func HeadBlock(ctx context.Context, rpcURL string) (uint64, error) { - body, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", "id": 1, - "method": "eth_blockNumber", "params": []any{}, - }) - var out struct { - Result string `json:"result"` - Error *struct{ Message string } `json:"error"` - } - if err := RpcPost(ctx, rpcURL, body, &out); err != nil { - return 0, err - } - if out.Error != nil { - return 0, fmt.Errorf("eth_blockNumber: %s", out.Error.Message) - } - return ParseHex64(out.Result) -} - -// BlockTime fetches a block's unix timestamp via eth_getBlockByNumber(n, false). -func BlockTime(ctx context.Context, rpcURL string, blockNum uint64) (time.Time, error) { - body, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", "id": 1, - "method": "eth_getBlockByNumber", - "params": []any{fmt.Sprintf("0x%x", blockNum), false}, - }) - var out struct { - Result *struct { - Timestamp string `json:"timestamp"` - } `json:"result"` - Error *struct{ Message string } `json:"error"` - } - if err := RpcPost(ctx, rpcURL, body, &out); err != nil { - return time.Time{}, err - } - if out.Error != nil { - return time.Time{}, fmt.Errorf("eth_getBlockByNumber: %s", out.Error.Message) - } - if out.Result == nil { - return time.Time{}, fmt.Errorf("null block %d", blockNum) - } - ts, err := ParseHex64(out.Result.Timestamp) - if err != nil { - return time.Time{}, err - } - return time.Unix(int64(ts), 0).UTC(), nil -} - -// ResolveBlockTag resolves an eth block tag ("finalized", "latest") to a block number. -func ResolveBlockTag(ctx context.Context, rpcURL, tag string) (uint64, error) { - body, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", "id": 1, - "method": "eth_getBlockByNumber", - "params": []any{tag, false}, - }) - var out struct { - Result *struct { - Number string `json:"number"` - } `json:"result"` - Error *struct{ Message string } `json:"error"` - } - if err := RpcPost(ctx, rpcURL, body, &out); err != nil { - return 0, err - } - if out.Error != nil { - return 0, fmt.Errorf("eth_getBlockByNumber %s: %s", tag, out.Error.Message) - } - if out.Result == nil { - return 0, fmt.Errorf("null block for tag %q", tag) - } - return ParseHex64(out.Result.Number) -} - -// RpcPost marshals body, POSTs to rpcURL with Content-Type: application/json, decodes into dst. -func RpcPost(ctx context.Context, rpcURL string, body []byte, dst any) error { - req, err := http.NewRequestWithContext(ctx, http.MethodPost, rpcURL, bytes.NewReader(body)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - resp, err := httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("HTTP %d from %s", resp.StatusCode, rpcURL) - } - return json.NewDecoder(resp.Body).Decode(dst) -} - -// ParseHex64 parses a 0x-prefixed or bare hex string to uint64. -func ParseHex64(s string) (uint64, error) { - return strconv.ParseUint(strings.TrimPrefix(s, "0x"), 16, 64) -} diff --git a/harnesses/evm-exec/internal/source/etherscan.go b/harnesses/evm-exec/internal/source/etherscan.go deleted file mode 100644 index 7790bed8..00000000 --- a/harnesses/evm-exec/internal/source/etherscan.go +++ /dev/null @@ -1,210 +0,0 @@ -package source - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "net/http" - "net/url" - "strings" - "time" -) - -const ( - etherscanV2 = "https://api.etherscan.io/v2/api" - ChainIDEthereum = "1" - ChainIDBase = "8453" // Base via Etherscan V2 (same key as Ethereum, no separate BaseScan key) -) - -// NativeTx is an ETH transfer (internal or normal) to a monitored address. -type NativeTx struct { - TxHash string - BlockNum uint64 - BlockTime time.Time // from Etherscan timeStamp, avoids eth_getBlockByNumber - Amount *big.Int // wei, never divided - EventKey string // traceId for internal txs, "" for normal value txs -} - -type etherscanTx struct { - Hash string `json:"hash"` - BlockNumber string `json:"blockNumber"` - TimeStamp string `json:"timeStamp"` // unix seconds as decimal string - Value string `json:"value"` - To string `json:"to"` - IsError string `json:"isError"` - TraceID string `json:"traceId"` -} - -// GetEtherscanInternalTxs fetches ETH internal transfers to address starting from startBlock. -func GetEtherscanInternalTxs(ctx context.Context, apiKey, chainID, address string, startBlock uint64) ([]NativeTx, uint64, error) { - return fetchEtherscanTxs(ctx, apiKey, chainID, address, startBlock, "txlistinternal", func(tx etherscanTx) (NativeTx, bool) { - if tx.IsError == "1" || tx.Value == "0" || tx.Value == "" { - return NativeTx{}, false - } - amt, err := ParseDecAmount(tx.Value) - if err != nil || amt.Sign() <= 0 { - return NativeTx{}, false - } - blockNum, err := parseDecU64(tx.BlockNumber) - if err != nil { - return NativeTx{}, false - } - var bt time.Time - if ts, err2 := parseDecU64(tx.TimeStamp); err2 == nil && ts > 0 { - bt = time.Unix(int64(ts), 0).UTC() - } - return NativeTx{TxHash: strings.ToLower(tx.Hash), BlockNum: blockNum, BlockTime: bt, Amount: amt, EventKey: tx.TraceID}, true - }) -} - -// GetEtherscanNormalTxs fetches plain ETH value transfers to address starting from startBlock. -func GetEtherscanNormalTxs(ctx context.Context, apiKey, chainID, address string, startBlock uint64) ([]NativeTx, uint64, error) { - addrLower := strings.ToLower(address) - return fetchEtherscanTxs(ctx, apiKey, chainID, address, startBlock, "txlist", func(tx etherscanTx) (NativeTx, bool) { - if tx.IsError == "1" || tx.Value == "0" || tx.Value == "" { - return NativeTx{}, false - } - if strings.ToLower(tx.To) != addrLower { - return NativeTx{}, false - } - amt, err := ParseDecAmount(tx.Value) - if err != nil || amt.Sign() <= 0 { - return NativeTx{}, false - } - blockNum, err := parseDecU64(tx.BlockNumber) - if err != nil { - return NativeTx{}, false - } - var bt time.Time - if ts, err2 := parseDecU64(tx.TimeStamp); err2 == nil && ts > 0 { - bt = time.Unix(int64(ts), 0).UTC() - } - return NativeTx{TxHash: strings.ToLower(tx.Hash), BlockNum: blockNum, BlockTime: bt, Amount: amt, EventKey: ""}, true - }) -} - -type filterFn func(etherscanTx) (NativeTx, bool) - -// fetchEtherscanTxs handles Etherscan pagination including the 10k-result cap. -func fetchEtherscanTxs(ctx context.Context, apiKey, chainID, address string, startBlock uint64, action string, filter filterFn) ([]NativeTx, uint64, error) { - const ( - offset = 1000 - maxPage = 10 - endBlock = 99999999 - ) - - var all []NativeTx - highestBlock := startBlock - curStart := startBlock - - for { - var windowTxs []NativeTx - hitCap := false - page := 1 - - for page <= maxPage { - time.Sleep(250 * time.Millisecond) - - batch, err := etherscanPage(ctx, apiKey, chainID, address, action, curStart, endBlock, page, offset) - if err != nil { - return all, highestBlock, fmt.Errorf("etherscan %s page %d: %w", action, page, err) - } - - for _, tx := range batch { - if ntx, ok := filter(tx); ok { - windowTxs = append(windowTxs, ntx) - if ntx.BlockNum > highestBlock { - highestBlock = ntx.BlockNum - } - } - } - - if len(batch) < offset { - break // last page - } - if page == maxPage { - hitCap = true - break - } - page++ - } - - all = append(all, windowTxs...) - - if !hitCap || len(windowTxs) == 0 { - break - } - - // Advance past what we've seen and loop. - var lastBlock uint64 - for _, tx := range windowTxs { - if tx.BlockNum > lastBlock { - lastBlock = tx.BlockNum - } - } - if lastBlock <= curStart { - break - } - curStart = lastBlock - } - - return all, highestBlock, nil -} - -func etherscanPage(ctx context.Context, apiKey, chainID, address, action string, startBlock uint64, endBlock, page, offset int) ([]etherscanTx, error) { - apiURL := etherscanV2 - params := url.Values{ - "module": {"account"}, - "action": {action}, - "address": {address}, - "startblock": {fmt.Sprintf("%d", startBlock)}, - "endblock": {fmt.Sprintf("%d", endBlock)}, - "page": {fmt.Sprintf("%d", page)}, - "offset": {fmt.Sprintf("%d", offset)}, - "sort": {"asc"}, - "apikey": {apiKey}, - } - if chainID != "" { - params.Set("chainid", chainID) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL+"?"+params.Encode(), nil) - if err != nil { - return nil, err - } - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var out struct { - Status string `json:"status"` - Message string `json:"message"` - Result json.RawMessage `json:"result"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return nil, fmt.Errorf("etherscan decode: %w", err) - } - if out.Status == "0" && strings.Contains(out.Message, "No transactions") { - return nil, nil - } - if out.Status != "1" { - var msg string - json.Unmarshal(out.Result, &msg) //nolint:errcheck - return nil, fmt.Errorf("etherscan %s: %s %s", action, out.Message, msg) - } - - var txs []etherscanTx - if err := json.Unmarshal(out.Result, &txs); err != nil { - return nil, fmt.Errorf("etherscan result decode: %w", err) - } - return txs, nil -} - -func parseDecU64(s string) (uint64, error) { - var n uint64 - _, err := fmt.Sscan(s, &n) - return n, err -} diff --git a/harnesses/evm-exec/internal/source/logs.go b/harnesses/evm-exec/internal/source/logs.go deleted file mode 100644 index c1e1f95f..00000000 --- a/harnesses/evm-exec/internal/source/logs.go +++ /dev/null @@ -1,176 +0,0 @@ -package source - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "strconv" - "strings" - "time" -) - -const transferTopic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" - -// ERC20Transfer is one token Transfer event emitted to a recipient. -type ERC20Transfer struct { - TxHash string - BlockNum uint64 - LogIndex uint64 - BlockTime time.Time // from blockTimestamp in getLogs response; zero if not present - Amount *big.Int // raw token units (never divided in Go) -} - -// RPCError is a JSON-RPC level error; used to detect range-too-large codes. -type RPCError struct { - Code int - Message string -} - -func (e *RPCError) Error() string { return fmt.Sprintf("RPC %d: %s", e.Code, e.Message) } - -// IsRangeError returns true when the RPC error means the block range is too wide. -func IsRangeError(err error) bool { - rpcErr, ok := err.(*RPCError) - if !ok { - return false - } - // -32005: range too large (Alchemy/Infura), -32602: invalid params (geth), - // -32614: range limit exceeded (base mainnet.base.org) - return rpcErr.Code == -32005 || rpcErr.Code == -32602 || rpcErr.Code == -32614 -} - -// GetERC20Transfers fetches Transfer events from [fromBlock, toBlock] for one window. -func GetERC20Transfers(ctx context.Context, rpcURL, tokenContract, recipient string, fromBlock, toBlock uint64) ([]ERC20Transfer, error) { - // topic2: 32-byte left-padded address (12 zero bytes + 20 addr bytes). - topic2 := "0x" + strings.Repeat("0", 24) + strings.TrimPrefix(strings.ToLower(recipient), "0x") - - body, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", "id": 1, - "method": "eth_getLogs", - "params": []any{map[string]any{ - "fromBlock": fmt.Sprintf("0x%x", fromBlock), - "toBlock": fmt.Sprintf("0x%x", toBlock), - "address": tokenContract, - "topics": []any{transferTopic0, nil, topic2}, - }}, - }) - - var out struct { - Result []struct { - TxHash string `json:"transactionHash"` - BlockNumber string `json:"blockNumber"` - BlockTimestamp string `json:"blockTimestamp"` // hex unix seconds; present on Base, absent on ETH/BSC - LogIndex string `json:"logIndex"` - Data string `json:"data"` - Topics []string `json:"topics"` - } `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - if err := RpcPost(ctx, rpcURL, body, &out); err != nil { - return nil, fmt.Errorf("eth_getLogs: %w", err) - } - if out.Error != nil { - return nil, &RPCError{Code: out.Error.Code, Message: out.Error.Message} - } - - var transfers []ERC20Transfer - for _, log := range out.Result { - if len(log.Data) < 2 || len(log.Topics) < 3 { - continue - } - blockNum, err := ParseHex64(log.BlockNumber) - if err != nil { - continue - } - logIdx, err := ParseHex64(log.LogIndex) - if err != nil { - continue - } - amount, ok := new(big.Int).SetString(strings.TrimPrefix(log.Data, "0x"), 16) - if !ok || amount.Sign() <= 0 { - continue - } - var bt time.Time - if ts, err2 := ParseHex64(log.BlockTimestamp); err2 == nil && ts > 0 { - bt = time.Unix(int64(ts), 0).UTC() - } - transfers = append(transfers, ERC20Transfer{ - TxHash: strings.ToLower(log.TxHash), - BlockNum: blockNum, - LogIndex: logIdx, - BlockTime: bt, - Amount: amount, - }) - } - return transfers, nil -} - -// GetERC20TransfersAdaptive fetches all ERC-20 transfers from fromBlock to toBlock -// using adaptive range sizing (halves on range error, grows on success). -func GetERC20TransfersAdaptive(ctx context.Context, rpcURL, tokenContract, recipient string, fromBlock, toBlock uint64) ([]ERC20Transfer, uint64, error) { - const maxRange = uint64(10_000) - rangeSize := maxRange - var all []ERC20Transfer - cur := fromBlock - - for cur <= toBlock { - end := cur + rangeSize - 1 - if end > toBlock { - end = toBlock - } - - batch, err := GetERC20Transfers(ctx, rpcURL, tokenContract, recipient, cur, end) - if err != nil { - if IsRangeError(err) && rangeSize > 100 { - rangeSize /= 2 - continue - } - select { - case <-ctx.Done(): - return all, cur - 1, ctx.Err() - case <-time.After(3 * time.Second): - } - batch, err = GetERC20Transfers(ctx, rpcURL, tokenContract, recipient, cur, end) - if err != nil { - return all, cur - 1, fmt.Errorf("eth_getLogs [%d,%d]: %w", cur, end, err) - } - } - - all = append(all, batch...) - if rangeSize < maxRange { - rangeSize = rangeSize * 125 / 100 - if rangeSize > maxRange { - rangeSize = maxRange - } - } - cur = end + 1 - } - return all, toBlock, nil -} - -// ParseDecAmount parses a decimal-string wei amount (Etherscan format) to *big.Int. -func ParseDecAmount(s string) (*big.Int, error) { - v, ok := new(big.Int).SetString(s, 10) - if !ok { - return nil, fmt.Errorf("bad decimal amount: %q", s) - } - return v, nil -} - -// ParseHexAmount parses a 0x-prefixed hex wei amount (NodeReal format) to *big.Int. -func ParseHexAmount(s string) (*big.Int, error) { - v, ok := new(big.Int).SetString(strings.TrimPrefix(s, "0x"), 16) - if !ok { - return nil, fmt.Errorf("bad hex amount: %q", s) - } - return v, nil -} - -// LogIndexKey returns the event_key string for an ERC-20 log. -func LogIndexKey(logIndex uint64) string { - return strconv.FormatUint(logIndex, 10) -} diff --git a/harnesses/evm-exec/internal/source/nodereal.go b/harnesses/evm-exec/internal/source/nodereal.go deleted file mode 100644 index 466a7429..00000000 --- a/harnesses/evm-exec/internal/source/nodereal.go +++ /dev/null @@ -1,133 +0,0 @@ -package source - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "strings" - "time" -) - -// AssetTransfer is one native-asset (BNB/ETH) transfer from NodeReal nr_getAssetTransfers. -type AssetTransfer struct { - TxHash string - BlockNum uint64 - BlockTime time.Time // from NodeReal blockTimeStamp (avoids separate RPC call) - Amount *big.Int // wei, never divided - EventKey string // position index within (tx_hash) group for uniqueness -} - -type nrTransfer struct { - BlockNum string `json:"blockNum"` - Hash string `json:"hash"` - Value string `json:"value"` - Category string `json:"category"` - BlockTimeStamp int64 `json:"blockTimeStamp"` // unix seconds, present when withMetadata:true -} - -// GetNativeTransfers fetches all native asset (BNB) transfers to toAddress in [fromBlock, toBlock] -// using NodeReal nr_getAssetTransfers with full pagination via pageKey. -func GetNativeTransfers(ctx context.Context, rpcURL, toAddress string, fromBlock, toBlock uint64) ([]AssetTransfer, error) { - var allRaw []nrTransfer - pageKey := "" - - for { - params := map[string]any{ - "fromBlock": fmt.Sprintf("0x%x", fromBlock), - "toBlock": fmt.Sprintf("0x%x", toBlock), - "toAddress": toAddress, - "category": []string{"external", "internal"}, - "withMetadata": true, - "excludeZeroValue": true, - "maxCount": "0x3e8", // 1000 - } - if pageKey != "" { - params["pageKey"] = pageKey - } - - body, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", "id": 1, - "method": "nr_getAssetTransfers", - "params": []any{params}, - }) - - var out struct { - Result *struct { - Transfers []nrTransfer `json:"transfers"` - PageKey string `json:"pageKey"` - } `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - - 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): - } - } - if err := RpcPost(ctx, rpcURL, body, &out); err != nil { - lastErr = err - continue - } - lastErr = nil - break - } - if lastErr != nil { - return nil, fmt.Errorf("nr_getAssetTransfers: %w", lastErr) - } - if out.Error != nil { - return nil, fmt.Errorf("nr_getAssetTransfers RPC %d: %s", out.Error.Code, out.Error.Message) - } - if out.Result == nil { - return nil, fmt.Errorf("nr_getAssetTransfers: null result") - } - - allRaw = append(allRaw, out.Result.Transfers...) - pageKey = out.Result.PageKey - if pageKey == "" { - break - } - } - - // Assign event_key as position within each tx_hash group (handles multicall). - txCount := make(map[string]int, len(allRaw)) - result := make([]AssetTransfer, 0, len(allRaw)) - - for _, t := range allRaw { - if t.Value == "" || t.Value == "0x0" { - continue - } - amt, err := ParseHexAmount(t.Value) - if err != nil || amt.Sign() <= 0 { - continue - } - blockNum, err := ParseHex64(t.BlockNum) - if err != nil { - continue - } - hash := strings.ToLower(t.Hash) - idx := txCount[hash] - txCount[hash]++ - - var blockTime time.Time - if t.BlockTimeStamp > 0 { - blockTime = time.Unix(t.BlockTimeStamp, 0).UTC() - } - - result = append(result, AssetTransfer{ - TxHash: hash, - BlockNum: blockNum, - BlockTime: blockTime, - Amount: amt, - EventKey: fmt.Sprintf("%d", idx), - }) - } - return result, nil -} diff --git a/harnesses/evm-exec/internal/store/store.go b/harnesses/evm-exec/internal/store/store.go deleted file mode 100644 index 35ad6595..00000000 --- a/harnesses/evm-exec/internal/store/store.go +++ /dev/null @@ -1,199 +0,0 @@ -package store - -import ( - "context" - "fmt" - "math/big" - "time" - - "github.com/jackc/pgx/v5/pgxpool" -) - -type DB struct { - pool *pgxpool.Pool -} - -func New(ctx context.Context, connStr string) (*DB, error) { - pool, err := pgxpool.New(ctx, connStr) - if err != nil { - return nil, fmt.Errorf("store: connect: %w", err) - } - return &DB{pool: pool}, nil -} - -func (db *DB) Close() { db.pool.Close() } - -// EVMEvent is one fee inflow to a platform's fee collector. -type EVMEvent struct { - Chain string - TxHash string - BlockNum uint64 - BlockTime time.Time - Platform string - Asset string // "native" or token contract (lowercase) - AmountRaw *big.Int // wei or raw token units; never divided in Go - Decimals int - EventKey string // log_index / traceId / transfer index / "" -} - -// UpsertEvent inserts an EVM fee event; skips silently on (chain,tx_hash,asset,event_key) conflict. -func (db *DB) UpsertEvent(ctx context.Context, e EVMEvent) error { - _, err := db.pool.Exec(ctx, ` - INSERT INTO evm_exec_events - (chain, tx_hash, block_num, block_time, platform, asset, amount_raw, decimals, event_key) - VALUES ($1,$2,$3,$4,$5,$6,$7::NUMERIC,$8,$9) - ON CONFLICT (chain, tx_hash, asset, event_key) DO NOTHING`, - e.Chain, e.TxHash, e.BlockNum, e.BlockTime, - e.Platform, e.Asset, e.AmountRaw.String(), e.Decimals, e.EventKey, - ) - if err != nil { - return fmt.Errorf("store: upsert %s/%s: %w", e.Chain, e.TxHash, err) - } - return nil -} - -// UpsertEvents bulk-upserts in a single transaction. -func (db *DB) UpsertEvents(ctx context.Context, events []EVMEvent) error { - if len(events) == 0 { - return nil - } - tx, err := db.pool.Begin(ctx) - if err != nil { - return fmt.Errorf("store: begin: %w", err) - } - defer tx.Rollback(ctx) - - for _, e := range events { - _, err := tx.Exec(ctx, ` - INSERT INTO evm_exec_events - (chain, tx_hash, block_num, block_time, platform, asset, amount_raw, decimals, event_key) - VALUES ($1,$2,$3,$4,$5,$6,$7::NUMERIC,$8,$9) - ON CONFLICT (chain, tx_hash, asset, event_key) DO NOTHING`, - e.Chain, e.TxHash, e.BlockNum, e.BlockTime, - e.Platform, e.Asset, e.AmountRaw.String(), e.Decimals, e.EventKey, - ) - if err != nil { - return fmt.Errorf("store: upsert %s/%s: %w", e.Chain, e.TxHash, err) - } - } - return tx.Commit(ctx) -} - -// GetCursor returns the last processed block for (chain, platform, asset). -// Returns (0, false, nil) when no cursor exists yet. -func (db *DB) GetCursor(ctx context.Context, chain, platform, asset string) (uint64, bool, error) { - var lastBlock uint64 - err := db.pool.QueryRow(ctx, - `SELECT last_block FROM evm_exec_cursors WHERE chain=$1 AND platform=$2 AND asset=$3`, - chain, platform, asset, - ).Scan(&lastBlock) - if err != nil { - return 0, false, nil - } - return lastBlock, true, nil -} - -// SaveCursor upserts the last processed block. -func (db *DB) SaveCursor(ctx context.Context, chain, platform, asset string, lastBlock uint64) error { - _, err := db.pool.Exec(ctx, ` - INSERT INTO evm_exec_cursors (chain, platform, asset, last_block, updated_at) - VALUES ($1,$2,$3,$4,now()) - ON CONFLICT (chain, platform, asset) DO UPDATE SET - last_block = EXCLUDED.last_block, updated_at = now()`, - chain, platform, asset, lastBlock, - ) - return err -} - -// SeedCursorIfAbsent inserts a bootstrap cursor only when none exists (DO NOTHING on conflict). -func (db *DB) SeedCursorIfAbsent(ctx context.Context, chain, platform, asset string, bootstrapBlock uint64) error { - _, err := db.pool.Exec(ctx, ` - INSERT INTO evm_exec_cursors (chain, platform, asset, last_block, updated_at) - VALUES ($1,$2,$3,$4,now()) - ON CONFLICT (chain, platform, asset) DO NOTHING`, - chain, platform, asset, bootstrapBlock, - ) - return err -} - -// GetBlockTime returns a cached block timestamp, if present. -func (db *DB) GetBlockTime(ctx context.Context, chain string, blockNum uint64) (time.Time, bool) { - var t time.Time - err := db.pool.QueryRow(ctx, - `SELECT block_time FROM evm_block_times WHERE chain=$1 AND block_num=$2`, - chain, blockNum, - ).Scan(&t) - return t, err == nil -} - -// CacheBlockTime writes a block timestamp to the local cache table. -func (db *DB) CacheBlockTime(ctx context.Context, chain string, blockNum uint64, t time.Time) error { - _, err := db.pool.Exec(ctx, ` - INSERT INTO evm_block_times (chain, block_num, block_time) - VALUES ($1,$2,$3) - ON CONFLICT (chain, block_num) DO NOTHING`, - chain, blockNum, t, - ) - return err -} - -// Materialize recomputes hourly revenue facts from the last 31 days of events. -func (db *DB) Materialize(ctx context.Context) error { - _, err := db.pool.Exec(ctx, ` - INSERT INTO evm_exec_facts - (chain, platform, bucket_start, revenue_stable, revenue_native, native_symbol, updated_at) - SELECT - chain, - platform, - date_trunc('hour', block_time) AS bucket_start, - COALESCE(SUM(amount_raw::numeric / (10::numeric ^ decimals)) FILTER (WHERE asset <> 'native'), 0), - COALESCE(SUM(amount_raw::numeric / (10::numeric ^ decimals)) FILTER (WHERE asset = 'native'), 0), - CASE WHEN chain = 'bsc' THEN 'BNB' ELSE 'ETH' END, - now() - FROM evm_exec_events - WHERE block_time > now() - INTERVAL '31 days' - GROUP BY 1, 2, 3 - ON CONFLICT (chain, platform, bucket_start) DO UPDATE SET - revenue_stable = EXCLUDED.revenue_stable, - revenue_native = EXCLUDED.revenue_native, - native_symbol = EXCLUDED.native_symbol, - updated_at = now()`, - ) - return err -} - -// RevenueRow is one (chain, platform) 24h revenue aggregate. -type RevenueRow struct { - Chain string - Platform string - RevenueStable float64 - RevenueNative float64 - NativeSymbol string -} - -// GetRevenue24h returns aggregated revenue for the last 24h per (chain, platform). -func (db *DB) GetRevenue24h(ctx context.Context) ([]RevenueRow, error) { - rows, err := db.pool.Query(ctx, ` - SELECT chain, platform, - COALESCE(SUM(revenue_stable), 0), - COALESCE(SUM(revenue_native), 0), - COALESCE(MAX(native_symbol), '') - FROM evm_exec_facts - WHERE bucket_start >= now() - INTERVAL '24 hours' - GROUP BY chain, platform`, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - var out []RevenueRow - for rows.Next() { - var r RevenueRow - if err := rows.Scan(&r.Chain, &r.Platform, &r.RevenueStable, &r.RevenueNative, &r.NativeSymbol); err != nil { - return nil, err - } - out = append(out, r) - } - return out, rows.Err() -} diff --git a/harnesses/evm-exec/migrations/004_evm_exec.sql b/harnesses/evm-exec/migrations/004_evm_exec.sql deleted file mode 100644 index ac7c879c..00000000 --- a/harnesses/evm-exec/migrations/004_evm_exec.sql +++ /dev/null @@ -1,46 +0,0 @@ -CREATE TABLE IF NOT EXISTS evm_exec_events ( - id BIGSERIAL PRIMARY KEY, - chain TEXT NOT NULL, - tx_hash TEXT NOT NULL, - block_num BIGINT NOT NULL, - block_time TIMESTAMPTZ NOT NULL, - platform TEXT NOT NULL, - asset TEXT NOT NULL, - amount_raw NUMERIC(78,0) NOT NULL, - decimals INT NOT NULL, - -- intra-tx discriminator: log_index for ERC-20, traceId for Etherscan internal, - -- position index for NodeReal transfers, "" for plain value txs. - event_key TEXT NOT NULL, - UNIQUE (chain, tx_hash, asset, event_key) -); - -CREATE INDEX IF NOT EXISTS idx_evm_events_lookup - ON evm_exec_events (platform, chain, block_time DESC); - -CREATE TABLE IF NOT EXISTS evm_block_times ( - chain TEXT NOT NULL, - block_num BIGINT NOT NULL, - block_time TIMESTAMPTZ NOT NULL, - PRIMARY KEY (chain, block_num) -); - --- No DEFAULT on last_block: the collector seeds from bootstrap on first run. -CREATE TABLE IF NOT EXISTS evm_exec_cursors ( - chain TEXT NOT NULL, - platform TEXT NOT NULL, - asset TEXT NOT NULL, - last_block BIGINT NOT NULL, - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (chain, platform, asset) -); - -CREATE TABLE IF NOT EXISTS evm_exec_facts ( - chain TEXT NOT NULL, - platform TEXT NOT NULL, - bucket_start TIMESTAMPTZ NOT NULL, - revenue_stable NUMERIC NOT NULL DEFAULT 0, - revenue_native NUMERIC NOT NULL DEFAULT 0, - native_symbol TEXT, - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (chain, platform, bucket_start) -); diff --git a/harnesses/memecoin-platforms/.env.example b/harnesses/memecoin-platforms/.env.example deleted file mode 100644 index dbb65e2d..00000000 --- a/harnesses/memecoin-platforms/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DUNE_API_KEY=your_dune_api_key -DUNE_QUERY_ID= # leave empty on first run; harness prints the ID and exits -MOBULA_API_KEY=your_mobula_api_key diff --git a/harnesses/memecoin-platforms/Dockerfile b/harnesses/memecoin-platforms/Dockerfile deleted file mode 100644 index 0f7bd539..00000000 --- a/harnesses/memecoin-platforms/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /build - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -o /memecoin-platforms ./cmd/monitor - -FROM alpine:latest - -RUN apk --no-cache add ca-certificates - -WORKDIR /app - -COPY --from=builder /memecoin-platforms . - -EXPOSE 2112 - -CMD ["./memecoin-platforms"] diff --git a/harnesses/memecoin-platforms/cmd/monitor/defillama.go b/harnesses/memecoin-platforms/cmd/monitor/defillama.go deleted file mode 100644 index 32fa0d1c..00000000 --- a/harnesses/memecoin-platforms/cmd/monitor/defillama.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -type defillamaClient struct { - http *http.Client -} - -func newDefillamaClient() *defillamaClient { - return &defillamaClient{http: &http.Client{Timeout: 15 * time.Second}} -} - -// fetchFomoFees returns Fomo's 24h Solana fees in USD from DeFiLlama. -// Fomo combines on-chain USDC inflows + off-chain relay fees via a private -// Dune table (dune.tryfomo.fomo_relay_fees) that only DeFiLlama can read. -func (c *defillamaClient) fetchFomoFees() (float64, error) { - req, err := http.NewRequest(http.MethodGet, "https://api.llama.fi/summary/fees/fomo", nil) - if err != nil { - return 0, err - } - resp, err := c.http.Do(req) - if err != nil { - return 0, fmt.Errorf("http: %w", err) - } - defer resp.Body.Close() - body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - if err != nil { - return 0, err - } - if resp.StatusCode >= 400 { - return 0, fmt.Errorf("status %d", resp.StatusCode) - } - var out struct { - ChainBreakdown map[string]struct { - Total24h float64 `json:"total24h"` - } `json:"chainBreakdown"` - } - if err := json.Unmarshal(body, &out); err != nil { - return 0, fmt.Errorf("parse: %w", err) - } - if bd, ok := out.ChainBreakdown["Solana"]; ok { - return bd.Total24h, nil - } - return 0, nil -} diff --git a/harnesses/memecoin-platforms/cmd/monitor/dune.go b/harnesses/memecoin-platforms/cmd/monitor/dune.go deleted file mode 100644 index 853eb815..00000000 --- a/harnesses/memecoin-platforms/cmd/monitor/dune.go +++ /dev/null @@ -1,292 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" -) - -// feesSQL is the Dune Trino SQL embedded here for auto-create. -// Mirrors harnesses/memecoin-platforms/queries/platform_fees.sql. -const feesSQL = ` -WITH - -wallet_platform AS ( - SELECT address, platform FROM (VALUES - ('BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT','gmgn'), - ('7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA','gmgn'), - ('HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3','gmgn'), - ('ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy','gmgn'), - ('DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9','gmgn'), - ('3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK','gmgn'), - ('DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx','gmgn'), - ('dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ','gmgn'), - ('6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn','gmgn'), - ('7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj','axiom'), - ('4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S','axiom'), - ('CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ','axiom'), - ('AaG6of1gbj1pbDumvbSiTuJhRCRkkUNaWVxijSbWvTJW','axiom'), - ('7oi1L8U9MRu5zDz5syFahsiLUric47LzvJBQX6r827ws','axiom'), - ('9kPrgLggBJ69tx1czYAbp7fezuUmL337BsqQTKETUEhP','axiom'), - ('DKyUs1xXMDy8Z11zNsLnUg3dy9HZf6hYZidB6WodcaGy','axiom'), - ('4FobGn5ZWYquoJkxMzh2VUAWvV36xMgxQ3M7uG1pGGhd','axiom'), - ('76sxKrPtgoJHDJvxwFHqb3cAXWfRHFLe3VpKcLCAHSEf','axiom'), - ('H2cDR3EkJjtTKDQKk8SJS48du9mhsdzQhy8xJx5UMqQK','axiom'), - ('8m5GkL7nVy95G4YVUbs79z873oVKqg2afgKRmqxsiiRm','axiom'), - ('4kuG6NsAFJNwqEkac8GFDMMheCGKUPEbaRVHHyFHSwWz','axiom'), - ('8vFGAKdwpn4hk7kc1cBgfWZzpyW3MEMDATDzVZhddeQb','axiom'), - ('86Vh4XGLW2b6nvWbRyDs4ScgMXbuvRCHT7WbUT3RFxKG','axiom'), - ('DZfEurFKFtSbdWZsKSDTqpqsQgvXxmESpvRtXkAdgLwM','axiom'), - ('5L2QKqDn5ukJSWGyqR4RPvFvwnBabKWqAqMzH4heaQNB','axiom'), - ('DYVeNgXGLAhZdeLMMYnCw1nPnMxkBN7fJnNpHmizTrrF','axiom'), - ('Hbj6XdxX6eV4nfbYTseysibp4zZJtVRRPn2J3BhGRuK9','axiom'), - ('846ah7iBSu9ApuCyEhA5xpnjHHX7d4QJKetWLbwzmJZ8','axiom'), - ('5BqYhuD4q1YD3DMAYkc1FeTu9vqQVYYdfBAmkZjamyZg','axiom'), - ('HrTf9CzXR1dRH4Sof5QrpmGWwpwAf3qZzwCsEjQpXcSq','fomo'), - ('9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco','trojan'), - ('92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH','trojan'), - ('2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ','trojan'), - ('65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH','trojan'), - ('BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp','trojan'), - ('8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn','trojan'), - ('AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH','photon'), - ('MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36','maestro'), - ('FRMxAnZgkW58zbYcE7Bxqsg99VWpJh6sMP5xLzAWNabN','maestro'), - ('CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM','pump-fun'), - ('62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV','pump-fun'), - ('FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz','pump-fun'), - ('7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX','pump-fun'), - ('AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY','pump-fun'), - ('9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz','pump-fun'), - ('G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP','pump-fun'), - ('7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ','pump-fun'), - ('GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS','pump-fun') - ) AS t(address, platform) -), - -native_sol AS ( - SELECT wp.platform, a.tx_id, CAST(a.post_balance - a.pre_balance AS DOUBLE) / 1e9 AS inflow, a.block_time - FROM solana.account_activity a - JOIN wallet_platform wp ON a.address = wp.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address IS NULL - AND a.post_balance > a.pre_balance -), - -usdc_inflows AS ( - SELECT wp.platform, a.tx_id, GREATEST(a.post_token_balance - a.pre_token_balance, 0) / 1e6 AS inflow, a.block_time - FROM solana.account_activity a - JOIN wallet_platform wp ON a.token_balance_owner = wp.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' - AND a.post_token_balance > a.pre_token_balance -), - -wsol_inflows AS ( - SELECT wp.platform, a.tx_id, GREATEST(a.post_token_balance - a.pre_token_balance, 0) / 1e9 AS inflow, a.block_time - FROM solana.account_activity a - JOIN wallet_platform wp ON a.token_balance_owner = wp.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address = 'So11111111111111111111111111111111111111112' - AND a.post_token_balance > a.pre_token_balance -), - -combined AS ( - SELECT platform, 0.0 AS usdc_inflow, 0.0 AS wsol_inflow, inflow AS sol_inflow, block_time FROM native_sol - UNION ALL - SELECT platform, inflow, 0.0, 0.0, block_time FROM usdc_inflows - UNION ALL - SELECT platform, 0.0, inflow, 0.0, block_time FROM wsol_inflows -), - -fee_tx_ids AS ( - SELECT platform, tx_id FROM native_sol - UNION - SELECT platform, tx_id FROM usdc_inflows - UNION - SELECT platform, tx_id FROM wsol_inflows -), - -fee_paying_vol AS ( - SELECT sub.platform, SUM(sub.max_swap) AS fee_paying_volume_usd_24h - FROM ( - SELECT ft.platform, ft.tx_id, MAX(COALESCE(ABS(t.amount_usd), 0)) AS max_swap - FROM fee_tx_ids ft - JOIN dex_solana.trades t ON t.tx_id = ft.tx_id - WHERE t.block_time >= NOW() - INTERVAL '1' DAY - GROUP BY ft.platform, ft.tx_id - ) sub - GROUP BY sub.platform -) - -SELECT - fees.platform, - fees.usdc_fees_24h, - fees.wsol_fees_24h, - fees.sol_fees_24h, - fees.data_freshness, - COALESCE(fpv.fee_paying_volume_usd_24h, 0) AS fee_paying_volume_usd_24h -FROM ( - SELECT - platform, - COALESCE(SUM(usdc_inflow), 0) AS usdc_fees_24h, - COALESCE(SUM(wsol_inflow), 0) AS wsol_fees_24h, - COALESCE(SUM(sol_inflow), 0) AS sol_fees_24h, - MAX(block_time) AS data_freshness - FROM combined - GROUP BY platform -) fees -LEFT JOIN fee_paying_vol fpv ON fpv.platform = fees.platform -ORDER BY fees.platform -` - -const duneBase = "https://api.dune.com/api/v1" - -type duneClient struct { - apiKey string - http *http.Client -} - -type duneRow struct { - Platform string `json:"platform"` - USDCFees24h float64 `json:"usdc_fees_24h"` - WSOLFees24h float64 `json:"wsol_fees_24h"` - SOLFees24h float64 `json:"sol_fees_24h"` - DataFreshness string `json:"data_freshness"` - FeePayingVolumeUSD24h float64 `json:"fee_paying_volume_usd_24h"` -} - -func isNoExecutionError(err error) bool { - if err == nil { - return false - } - s := err.Error() - return strings.HasPrefix(s, "HTTP 40") && strings.Contains(s, "No execution") -} - -func newDuneClient(apiKey string) *duneClient { - return &duneClient{ - apiKey: apiKey, - http: &http.Client{Timeout: 30 * time.Second}, - } -} - -func (d *duneClient) req(method, path string, body any) (*http.Response, error) { - var r io.Reader - if body != nil { - b, err := json.Marshal(body) - if err != nil { - return nil, err - } - r = bytes.NewReader(b) - } - req, err := http.NewRequest(method, duneBase+path, r) - if err != nil { - return nil, err - } - req.Header.Set("X-Dune-Api-Key", d.apiKey) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - return d.http.Do(req) -} - -func (d *duneClient) createQuery() (string, error) { - payload := map[string]any{ - "name": "OCB: Memecoin Platform Fees 24h", - "query_sql": feesSQL, - "is_private": false, - "parameters": []any{}, - } - resp, err := d.req("POST", "/query", payload) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - b, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, b) - } - var out struct { - QueryID int `json:"query_id"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", err - } - return fmt.Sprintf("%d", out.QueryID), nil -} - -func (d *duneClient) updateQuery(queryID string) error { - payload := map[string]any{ - "query_sql": feesSQL, - } - resp, err := d.req("PATCH", "/query/"+queryID, payload) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - b, _ := io.ReadAll(resp.Body) - return fmt.Errorf("HTTP %d: %s", resp.StatusCode, b) - } - return nil -} - -func (d *duneClient) execute(queryID string) (string, error) { - resp, err := d.req("POST", "/query/"+queryID+"/execute", map[string]any{}) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - b, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, b) - } - var out struct { - ExecutionID string `json:"execution_id"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", err - } - return out.ExecutionID, nil -} - -func (d *duneClient) executionState(execID string) (string, error) { - resp, err := d.req("GET", "/execution/"+execID+"/status", nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - var out struct { - State string `json:"state"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", err - } - return out.State, nil -} - -func (d *duneClient) latestResult(queryID string) ([]duneRow, error) { - resp, err := d.req("GET", "/query/"+queryID+"/results", nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - b, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, b) - } - var out struct { - Result struct { - Rows []duneRow `json:"rows"` - } `json:"result"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return nil, err - } - return out.Result.Rows, nil -} diff --git a/harnesses/memecoin-platforms/cmd/monitor/lighthouse.go b/harnesses/memecoin-platforms/cmd/monitor/lighthouse.go deleted file mode 100644 index 7c947c40..00000000 --- a/harnesses/memecoin-platforms/cmd/monitor/lighthouse.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" -) - -const lighthouseURL = "https://api.mobula.io/api/2/market/lighthouse" - -type lighthouseClient struct { - apiKey string - http *http.Client -} - -func newLighthouseClient(apiKey string) *lighthouseClient { - return &lighthouseClient{ - apiKey: apiKey, - http: &http.Client{Timeout: 15 * time.Second}, - } -} - -// fetchVolumes returns platform name -> volume_usd_24h from the Mobula lighthouse. -func (c *lighthouseClient) fetchVolumes() (map[string]float64, error) { - req, err := http.NewRequest(http.MethodGet, lighthouseURL, nil) - if err != nil { - return nil, fmt.Errorf("build req: %w", err) - } - req.Header.Set("Authorization", c.apiKey) - req.Header.Set("Accept", "application/json") - - resp, err := c.http.Do(req) - if err != nil { - return nil, fmt.Errorf("http: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) - if err != nil { - return nil, fmt.Errorf("read: %w", err) - } - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("status %d: %.200s", resp.StatusCode, body) - } - - type entry struct { - Name string `json:"name"` - VolumeUSD struct { - H24 float64 `json:"24h"` - } `json:"volumeUSD"` - } - var out struct { - Data struct { - ByPlatform []entry `json:"byPlatform"` - ByLaunchpad []entry `json:"byLaunchpad"` - } `json:"data"` - } - if err := json.Unmarshal(body, &out); err != nil { - return nil, fmt.Errorf("parse: %w", err) - } - - volumes := make(map[string]float64) - for _, p := range out.Data.ByPlatform { - if p.Name == "" { - continue - } - volumes[strings.ToLower(p.Name)] = p.VolumeUSD.H24 - } - // pump.fun bonding curve is under byLaunchpad as "PumpFun" - for _, p := range out.Data.ByLaunchpad { - key := strings.ToLower(p.Name) - if key == "pumpfun" { - volumes["pump-fun"] = p.VolumeUSD.H24 - } - } - return volumes, nil -} diff --git a/harnesses/memecoin-platforms/cmd/monitor/main.go b/harnesses/memecoin-platforms/cmd/monitor/main.go deleted file mode 100644 index c266ce8c..00000000 --- a/harnesses/memecoin-platforms/cmd/monitor/main.go +++ /dev/null @@ -1,236 +0,0 @@ -// memecoin-platforms -- Bench 203 -// -// Computes per-platform take rates for Solana memecoin trading bots by combining: -// - Dune Analytics: fee wallet inflows (USDC + WSOL + native SOL) -> fees_usd_24h -// - Mobula lighthouse: platform trading volume -> volume_usd_24h -// - take_rate = fees_usd_24h / volume_usd_24h * 100 -// -// Required env vars: -// -// DUNE_API_KEY - Dune Analytics API key -// DUNE_QUERY_ID - Dune query ID (leave empty on first run; harness prints it and exits) -// MOBULA_API_KEY - Mobula API key -// -// Metrics on :2112/metrics: -// -// memecoin_platform_fee_rate_pct{platform} -// memecoin_platform_fees_usd_24h{platform} -// memecoin_platform_volume_usd_24h{platform} -// memecoin_platform_health -// memecoin_last_poll_timestamp_seconds -package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - "time" -) - -const ( - fetchInterval = 15 * time.Minute - refreshInterval = 6 * time.Hour - solPriceRefresh = 5 * time.Minute -) - -func main() { - fmt.Println("=== memecoin-platforms harness ===") - fmt.Println("OpenChainBench Bench 203 -- Memecoin platform fee rates via Dune + Mobula lighthouse.") - - duneKey := os.Getenv("DUNE_API_KEY") - if duneKey == "" { - fmt.Fprintln(os.Stderr, "[fatal] DUNE_API_KEY not set") - os.Exit(1) - } - mobulaKey := os.Getenv("MOBULA_API_KEY") - if mobulaKey == "" { - fmt.Fprintln(os.Stderr, "[fatal] MOBULA_API_KEY not set") - os.Exit(1) - } - - queryID := os.Getenv("DUNE_QUERY_ID") - dune := newDuneClient(duneKey) - - if queryID == "" { - fmt.Println("[init] DUNE_QUERY_ID not set, creating Dune query...") - id, err := dune.createQuery() - if err != nil { - fmt.Fprintf(os.Stderr, "[fatal] failed to create Dune query: %v\n", err) - os.Exit(1) - } - fmt.Printf("[init] Dune query created: %s\n", id) - fmt.Printf("[init] Set DUNE_QUERY_ID=%s and restart the harness.\n", id) - os.Exit(0) - } - - lighthouse := newLighthouseClient(mobulaKey) - llama := newDefillamaClient() - - startSolPriceRefresher(solPriceRefresh) - - go func() { - if err := startMetricsServer(":2112"); err != nil { - fmt.Printf("[fatal] metrics server: %v\n", err) - os.Exit(1) - } - }() - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - - // Sync Dune query SQL to the embedded feesSQL constant, then trigger a fresh execution. - // This ensures new SQL columns (e.g. fee_paying_volume_usd_24h) are picked up automatically. - if err := dune.updateQuery(queryID); err != nil { - fmt.Printf("[init] Dune query update failed (continuing with cached SQL): %v\n", err) - } else { - fmt.Println("[init] Dune query SQL synced") - } - - // On startup: fetch cached Dune result + current lighthouse data immediately, - // then trigger a fresh Dune execution in the background. - runPoll(dune, lighthouse, llama, queryID) - go func() { - execID, err := dune.execute(queryID) - if err != nil { - fmt.Printf("[refresh] execute failed: %v\n", err) - return - } - pollUntilDone(dune, lighthouse, llama, queryID, execID) - }() - - fetchTick := time.NewTicker(fetchInterval) - refreshTick := time.NewTicker(refreshInterval) - defer fetchTick.Stop() - defer refreshTick.Stop() - - for { - select { - case <-sig: - fmt.Println("[shutdown] received signal") - return - case <-fetchTick.C: - runPoll(dune, lighthouse, llama, queryID) - case <-refreshTick.C: - execID, err := dune.execute(queryID) - if err != nil { - fmt.Printf("[refresh] execute failed: %v\n", err) - continue - } - go pollUntilDone(dune, lighthouse, llama, queryID, execID) - } - } -} - -func runPoll(dune *duneClient, lh *lighthouseClient, llama *defillamaClient, queryID string) { - rows, err := dune.latestResult(queryID) - if err != nil { - if isNoExecutionError(err) { - // After a SQL update, Dune creates a new version with no execution yet. - // The background goroutine will execute and call runPoll when done. - fmt.Printf("[fetch] dune: no execution for current query version, waiting for background refresh\n") - } else { - fmt.Printf("[fetch] dune failed: %v\n", err) - platformHealth.Set(0) - } - return - } - - volumes, err := lh.fetchVolumes() - if err != nil { - fmt.Printf("[fetch] lighthouse failed: %v\n", err) - platformHealth.Set(0) - return - } - - solUSD := getSolPrice() - - // Aggregate rows per platform (Dune may return multiple rows). - type totals struct { - feesUSD float64 - feePayingVolumeUSD float64 - } - byPlatform := make(map[string]*totals) - var latestBlockTime string - for _, r := range rows { - if r.Platform == "" { - continue - } - t, ok := byPlatform[r.Platform] - if !ok { - t = &totals{} - byPlatform[r.Platform] = t - } - t.feesUSD += r.USDCFees24h + r.WSOLFees24h*solUSD + r.SOLFees24h*solUSD - t.feePayingVolumeUSD += r.FeePayingVolumeUSD24h - if r.DataFreshness > latestBlockTime { - latestBlockTime = r.DataFreshness - } - } - if latestBlockTime != "" { - s := latestBlockTime - if len(s) > 19 { - s = s[:19] - } - for _, layout := range []string{"2006-01-02T15:04:05", "2006-01-02 15:04:05"} { - if ts, err := time.Parse(layout, s); err == nil { - duneDataFreshness.Set(float64(ts.Unix())) - break - } - } - } - - // Fomo fees: on-chain USDC sweeps + off-chain relay fees only accessible - // via DeFiLlama (private Dune table dune.tryfomo.fomo_relay_fees). - fomoFees, err := llama.fetchFomoFees() - if err != nil { - fmt.Printf("[fetch] fomo/defillama failed: %v\n", err) - } else if fomoFees > 0 { - byPlatform["fomo"] = &totals{feesUSD: fomoFees} - } - - for platform, t := range byPlatform { - mobulaVol := volumes[platform] - feesUSD24h.WithLabelValues(platform).Set(t.feesUSD) - volumeUSD24h.WithLabelValues(platform).Set(mobulaVol) - feePayingVolumeUSD24h.WithLabelValues(platform).Set(t.feePayingVolumeUSD) - - rate := 0.0 - if mobulaVol > 0 { - rate = t.feesUSD / mobulaVol * 100 - } - feeRatePct.WithLabelValues(platform).Set(rate) - - if t.feePayingVolumeUSD > 0 { - feePayingRatePct.WithLabelValues(platform).Set(t.feesUSD / t.feePayingVolumeUSD * 100) - if mobulaVol > 0 { - coveragePct.WithLabelValues(platform).Set(t.feePayingVolumeUSD / mobulaVol * 100) - } - } - } - - lastPollTime.SetToCurrentTime() - platformHealth.Set(1) - fmt.Printf("[fetch] updated %d platform(s), sol=$%.2f\n", len(byPlatform), solUSD) -} - -func pollUntilDone(dune *duneClient, lh *lighthouseClient, llama *defillamaClient, queryID, execID string) { - for range 30 { - time.Sleep(30 * time.Second) - state, err := dune.executionState(execID) - if err != nil { - fmt.Printf("[poll] state check failed: %v\n", err) - return - } - switch state { - case "QUERY_STATE_COMPLETED": - fmt.Printf("[poll] execution %s complete\n", execID) - runPoll(dune, lh, llama, queryID) - return - case "QUERY_STATE_FAILED", "QUERY_STATE_CANCELLED", "QUERY_STATE_EXPIRED": - fmt.Printf("[poll] execution %s ended with state %s\n", execID, state) - return - } - } - fmt.Printf("[poll] execution %s timed out waiting\n", execID) -} diff --git a/harnesses/memecoin-platforms/cmd/monitor/metrics.go b/harnesses/memecoin-platforms/cmd/monitor/metrics.go deleted file mode 100644 index 87225b2e..00000000 --- a/harnesses/memecoin-platforms/cmd/monitor/metrics.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - feeRatePct = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "memecoin_platform_fee_rate_pct", - Help: "Take rate using Mobula total volume: fees_usd_24h / mobula_volume_usd_24h * 100 (lower bound; denominator includes non-fee trades)", - }, []string{"platform"}) - - feePayingRatePct = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "memecoin_platform_fee_paying_rate_pct", - Help: "Take rate using fee-paying volume only: fees_usd_24h / fee_paying_volume_usd_24h * 100 (comparable across platforms)", - }, []string{"platform"}) - - coveragePct = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "memecoin_platform_coverage_pct", - Help: "Fee-paying volume / Mobula total volume * 100: fraction of attributed volume that generated a fee inflow", - }, []string{"platform"}) - - feesUSD24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "memecoin_platform_fees_usd_24h", - Help: "Raw USD fees collected by this platform in the last 24h (Dune)", - }, []string{"platform"}) - - volumeUSD24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "memecoin_platform_volume_usd_24h", - Help: "Trading volume in USD in the last 24h (Mobula lighthouse, total attributed volume)", - }, []string{"platform"}) - - feePayingVolumeUSD24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "memecoin_platform_fee_paying_volume_usd_24h", - Help: "Volume of trades that generated a fee inflow in the last 24h (Dune: largest swap per fee-receiving tx)", - }, []string{"platform"}) - - platformHealth = prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "memecoin_platform_health", - Help: "1.0 when both Dune and lighthouse data are fresh", - }) - - lastPollTime = prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "memecoin_last_poll_timestamp_seconds", - Help: "Unix timestamp of the last successful poll", - }) - - duneDataFreshness = prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "memecoin_dune_data_freshness_seconds", - Help: "Unix timestamp of the most recent block_time seen in the Dune query results. Gap from now() indicates solana.account_activity table lag.", - }) -) - -func init() { - prometheus.MustRegister( - feeRatePct, feePayingRatePct, coveragePct, - feesUSD24h, volumeUSD24h, feePayingVolumeUSD24h, - platformHealth, lastPollTime, duneDataFreshness, - ) -} - -func startMetricsServer(addr string) error { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("OK")) - }) - return http.ListenAndServe(addr, mux) -} diff --git a/harnesses/memecoin-platforms/cmd/monitor/solprice.go b/harnesses/memecoin-platforms/cmd/monitor/solprice.go deleted file mode 100644 index 2a057a73..00000000 --- a/harnesses/memecoin-platforms/cmd/monitor/solprice.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "sync/atomic" - "time" - "math" -) - -const coingeckoURL = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" - -// solPriceBits stores the current SOL/USD price as a uint64 (math.Float64bits). -var solPriceBits atomic.Uint64 - -func init() { - setSolPrice(150.0) -} - -func setSolPrice(usd float64) { - solPriceBits.Store(math.Float64bits(usd)) -} - -func getSolPrice() float64 { - return math.Float64frombits(solPriceBits.Load()) -} - -func updateSolPrice(client *http.Client) { - price, err := fetchSolPrice(client) - if err != nil { - fmt.Printf("[solprice] fetch failed: %v (keeping %.2f)\n", err, getSolPrice()) - return - } - setSolPrice(price) - fmt.Printf("[solprice] updated: $%.2f\n", price) -} - -func fetchSolPrice(client *http.Client) (float64, error) { - req, err := http.NewRequest(http.MethodGet, coingeckoURL, nil) - if err != nil { - return 0, err - } - req.Header.Set("Accept", "application/json") - - resp, err := client.Do(req) - if err != nil { - return 0, fmt.Errorf("coingecko: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<16)) - if err != nil { - return 0, fmt.Errorf("read: %w", err) - } - if resp.StatusCode >= 400 { - return 0, fmt.Errorf("status %d: %.100s", resp.StatusCode, body) - } - - var out struct { - Solana struct { - USD float64 `json:"usd"` - } `json:"solana"` - } - if err := json.Unmarshal(body, &out); err != nil { - return 0, fmt.Errorf("parse: %w", err) - } - if out.Solana.USD <= 0 { - return 0, fmt.Errorf("invalid price: %v", out.Solana.USD) - } - return out.Solana.USD, nil -} - -func startSolPriceRefresher(interval time.Duration) { - client := &http.Client{Timeout: 10 * time.Second} - updateSolPrice(client) - go func() { - ticker := time.NewTicker(interval) - defer ticker.Stop() - for range ticker.C { - updateSolPrice(client) - } - }() -} diff --git a/harnesses/memecoin-platforms/go.mod b/harnesses/memecoin-platforms/go.mod deleted file mode 100644 index f7eb7f34..00000000 --- a/harnesses/memecoin-platforms/go.mod +++ /dev/null @@ -1,19 +0,0 @@ -module memecoin-platforms - -go 1.24.0 - -toolchain go1.24.4 - -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.9 // 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.55.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - golang.org/x/sys v0.40.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect -) diff --git a/harnesses/memecoin-platforms/go.sum b/harnesses/memecoin-platforms/go.sum deleted file mode 100644 index 309d820f..00000000 --- a/harnesses/memecoin-platforms/go.sum +++ /dev/null @@ -1,24 +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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -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/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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/harnesses/memecoin-platforms/queries/platform_fees.sql b/harnesses/memecoin-platforms/queries/platform_fees.sql deleted file mode 100644 index 9f440be3..00000000 --- a/harnesses/memecoin-platforms/queries/platform_fees.sql +++ /dev/null @@ -1,144 +0,0 @@ --- OCB Bench 203: Memecoin Platform Fees (Dune Trino SQL) --- Monitors known fee wallets for native SOL, USDC, and WSOL inflows. --- Key: native SOL uses address column, SPL tokens use token_balance_owner column. --- --- Columns: platform, usdc_fees_24h, wsol_fees_24h, sol_fees_24h, data_freshness, --- fee_paying_volume_usd_24h --- fee_paying_volume_usd_24h = sum of the largest swap (amount_usd) in each tx that --- generated a fee inflow; provides a self-consistent denominator so take_rate is --- comparable across platforms (vs. Mobula total volume which includes non-fee trades). --- data_freshness = MAX(block_time) across all matched rows; alerts if table lags --- and the nominal 24h window silently shrinks (e.g. table only refreshed 20h ago). - -WITH - -wallet_platform AS ( - SELECT address, platform FROM (VALUES - ('BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT','gmgn'), - ('7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA','gmgn'), - ('HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3','gmgn'), - ('ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy','gmgn'), - ('DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9','gmgn'), - ('3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK','gmgn'), - ('DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx','gmgn'), - ('dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ','gmgn'), - ('6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn','gmgn'), - ('7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj','axiom'), - ('4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S','axiom'), - ('CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ','axiom'), - ('AaG6of1gbj1pbDumvbSiTuJhRCRkkUNaWVxijSbWvTJW','axiom'), - ('7oi1L8U9MRu5zDz5syFahsiLUric47LzvJBQX6r827ws','axiom'), - ('9kPrgLggBJ69tx1czYAbp7fezuUmL337BsqQTKETUEhP','axiom'), - ('DKyUs1xXMDy8Z11zNsLnUg3dy9HZf6hYZidB6WodcaGy','axiom'), - ('4FobGn5ZWYquoJkxMzh2VUAWvV36xMgxQ3M7uG1pGGhd','axiom'), - ('76sxKrPtgoJHDJvxwFHqb3cAXWfRHFLe3VpKcLCAHSEf','axiom'), - ('H2cDR3EkJjtTKDQKk8SJS48du9mhsdzQhy8xJx5UMqQK','axiom'), - ('8m5GkL7nVy95G4YVUbs79z873oVKqg2afgKRmqxsiiRm','axiom'), - ('4kuG6NsAFJNwqEkac8GFDMMheCGKUPEbaRVHHyFHSwWz','axiom'), - ('8vFGAKdwpn4hk7kc1cBgfWZzpyW3MEMDATDzVZhddeQb','axiom'), - ('86Vh4XGLW2b6nvWbRyDs4ScgMXbuvRCHT7WbUT3RFxKG','axiom'), - ('DZfEurFKFtSbdWZsKSDTqpqsQgvXxmESpvRtXkAdgLwM','axiom'), - ('5L2QKqDn5ukJSWGyqR4RPvFvwnBabKWqAqMzH4heaQNB','axiom'), - ('DYVeNgXGLAhZdeLMMYnCw1nPnMxkBN7fJnNpHmizTrrF','axiom'), - ('Hbj6XdxX6eV4nfbYTseysibp4zZJtVRRPn2J3BhGRuK9','axiom'), - ('846ah7iBSu9ApuCyEhA5xpnjHHX7d4QJKetWLbwzmJZ8','axiom'), - ('5BqYhuD4q1YD3DMAYkc1FeTu9vqQVYYdfBAmkZjamyZg','axiom'), - ('HrTf9CzXR1dRH4Sof5QrpmGWwpwAf3qZzwCsEjQpXcSq','fomo'), - ('9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco','trojan'), - ('92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH','trojan'), - ('2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ','trojan'), - ('65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH','trojan'), - ('BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp','trojan'), - ('8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn','trojan'), - ('AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH','photon'), - ('MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36','maestro'), - ('FRMxAnZgkW58zbYcE7Bxqsg99VWpJh6sMP5xLzAWNabN','maestro'), - ('CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM','pump-fun'), - ('62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV','pump-fun'), - ('FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz','pump-fun'), - ('7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX','pump-fun'), - ('AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY','pump-fun'), - ('9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz','pump-fun'), - ('G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP','pump-fun'), - ('7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ','pump-fun'), - ('GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS','pump-fun') - ) AS t(address, platform) -), - -native_sol AS ( - SELECT wp.platform, a.tx_id, CAST(a.post_balance - a.pre_balance AS DOUBLE) / 1e9 AS inflow, a.block_time - FROM solana.account_activity a - JOIN wallet_platform wp ON a.address = wp.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address IS NULL - AND a.post_balance > a.pre_balance -), - -usdc_inflows AS ( - SELECT wp.platform, a.tx_id, GREATEST(a.post_token_balance - a.pre_token_balance, 0) / 1e6 AS inflow, a.block_time - FROM solana.account_activity a - JOIN wallet_platform wp ON a.token_balance_owner = wp.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' - AND a.post_token_balance > a.pre_token_balance -), - -wsol_inflows AS ( - SELECT wp.platform, a.tx_id, GREATEST(a.post_token_balance - a.pre_token_balance, 0) / 1e9 AS inflow, a.block_time - FROM solana.account_activity a - JOIN wallet_platform wp ON a.token_balance_owner = wp.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address = 'So11111111111111111111111111111111111111112' - AND a.post_token_balance > a.pre_token_balance -), - -combined AS ( - SELECT platform, 0.0 AS usdc_inflow, 0.0 AS wsol_inflow, inflow AS sol_inflow, block_time FROM native_sol - UNION ALL - SELECT platform, inflow, 0.0, 0.0, block_time FROM usdc_inflows - UNION ALL - SELECT platform, 0.0, inflow, 0.0, block_time FROM wsol_inflows -), - --- All (platform, tx_id) pairs where a fee inflow was received (deduplicated) -fee_tx_ids AS ( - SELECT platform, tx_id FROM native_sol - UNION - SELECT platform, tx_id FROM usdc_inflows - UNION - SELECT platform, tx_id FROM wsol_inflows -), - --- For each fee-generating tx, take the largest swap trade (max amount_usd per tx) --- to avoid double-counting multi-hop swaps in the same transaction. -fee_paying_vol AS ( - SELECT sub.platform, SUM(sub.max_swap) AS fee_paying_volume_usd_24h - FROM ( - SELECT ft.platform, ft.tx_id, MAX(COALESCE(ABS(t.amount_usd), 0)) AS max_swap - FROM fee_tx_ids ft - JOIN dex_solana.trades t ON t.tx_id = ft.tx_id - WHERE t.block_time >= NOW() - INTERVAL '1' DAY - GROUP BY ft.platform, ft.tx_id - ) sub - GROUP BY sub.platform -) - -SELECT - fees.platform, - fees.usdc_fees_24h, - fees.wsol_fees_24h, - fees.sol_fees_24h, - fees.data_freshness, - COALESCE(fpv.fee_paying_volume_usd_24h, 0) AS fee_paying_volume_usd_24h -FROM ( - SELECT - platform, - COALESCE(SUM(usdc_inflow), 0) AS usdc_fees_24h, - COALESCE(SUM(wsol_inflow), 0) AS wsol_fees_24h, - COALESCE(SUM(sol_inflow), 0) AS sol_fees_24h, - MAX(block_time) AS data_freshness - FROM combined - GROUP BY platform -) fees -LEFT JOIN fee_paying_vol fpv ON fpv.platform = fees.platform -ORDER BY fees.platform diff --git a/harnesses/metadata-coverage/cmd/script/defined_auth.go b/harnesses/metadata-coverage/cmd/script/defined_auth.go index 40ca7c73..717e75ad 100644 --- a/harnesses/metadata-coverage/cmd/script/defined_auth.go +++ b/harnesses/metadata-coverage/cmd/script/defined_auth.go @@ -58,33 +58,6 @@ func decodeJWTExpiration(token string) (time.Time, error) { return time.Unix(claims.Exp, 0), nil } -// tryTokenService fetches a fresh JWE from the Paris-box sidecar. -func tryTokenService(baseURL string) (string, error) { - client := &http.Client{Timeout: 5 * time.Second} - for _, path := range []string{"/token", "/jwt", "/"} { - resp, err := client.Get(baseURL + path) - if err != nil { - continue - } - body, _ := io.ReadAll(resp.Body) - resp.Body.Close() - if resp.StatusCode != 200 { - continue - } - var parsed struct { - Token string `json:"token"` - } - if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { - return parsed.Token, nil - } - s := strings.TrimSpace(string(body)) - if len(s) > 50 { - return s, nil - } - } - return "", fmt.Errorf("no working path on sidecar") -} - // GetDefinedJWTToken returns a cached JWT token or generates a new one if expired. // If CODEX_JWT env var is set, it is returned directly (bypasses session-cookie flow). func GetDefinedJWTToken(sessionCookie string) (string, error) { @@ -111,17 +84,6 @@ func GetDefinedJWTToken(sessionCookie string) (string, error) { return globalTokenCache.token, nil } - // Sidecar (Paris box, fresh every 8 min via Tor+utls, bypasses Kasada) - 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)) - globalTokenCache.token = tok - globalTokenCache.expiresAt = time.Now().Add(8 * time.Minute) - globalTokenCache.lastRefresh = time.Now() - return tok, nil - } - } - // Generate new token token, err := generateDefinedJWTToken(sessionCookie) if err != nil { diff --git a/harnesses/mev-protect-rpc/cmd/script/probe.go b/harnesses/mev-protect-rpc/cmd/script/probe.go index 0afd3358..25920a4d 100644 --- a/harnesses/mev-protect-rpc/cmd/script/probe.go +++ b/harnesses/mev-protect-rpc/cmd/script/probe.go @@ -192,7 +192,7 @@ func call(ctx context.Context, client *http.Client, url, method string, params [ start := time.Now() resp, err := client.Do(req) - lat := float64(time.Since(start).Nanoseconds()) / 1e6 + lat := float64(time.Since(start).Milliseconds()) if err != nil { if ctx.Err() != nil { return lat, "canceled" diff --git a/harnesses/perp-protocol-longevity/cmd/script/defillama.go b/harnesses/perp-protocol-longevity/cmd/script/defillama.go index c07becb2..e54b5962 100644 --- a/harnesses/perp-protocol-longevity/cmd/script/defillama.go +++ b/harnesses/perp-protocol-longevity/cmd/script/defillama.go @@ -65,9 +65,6 @@ func enrichFromDefiLlama(reg []venueRecord) []venueRecord { if t.Before(reg[i].Launched) { continue } - if h.Amount <= 0 { - continue // null/zero amount = no confirmed financial loss - } if h.ReturnedFunds != nil && *h.ReturnedFunds >= h.Amount { continue } diff --git a/harnesses/pm-rate-limits/cmd/script/codex_auth.go b/harnesses/pm-rate-limits/cmd/script/codex_auth.go index 2b170cd7..a4e1fd39 100644 --- a/harnesses/pm-rate-limits/cmd/script/codex_auth.go +++ b/harnesses/pm-rate-limits/cmd/script/codex_auth.go @@ -78,18 +78,6 @@ func GetCodexJWT(sessionCookie string) (string, error) { if codexTokenCache.token != "" && time.Now().Before(codexTokenCache.expiresAt.Add(-1*time.Hour)) { return codexTokenCache.token, nil } - - // Sidecar (Paris box, fresh every 8 min via Tor+utls, bypasses Kasada) - if svcURL := os.Getenv("DEFINED_TOKEN_SERVICE_URL"); svcURL != "" { - if tok, err := tryTokenService(svcURL); err == nil && tok != "" { - log.Printf("[codex-auth] Got token from sidecar (len=%d)", len(tok)) - codexTokenCache.token = tok - codexTokenCache.expiresAt = time.Now().Add(8 * time.Minute) - codexTokenCache.lastRefresh = time.Now() - return tok, nil - } - } - tok, err := mintCodexJWT(sessionCookie) if err != nil { return "", err @@ -113,32 +101,6 @@ func InvalidateCodexJWT() { log.Printf("[codex-auth] JWT cache invalidated") } -func tryTokenService(baseURL string) (string, error) { - client := &http.Client{Timeout: 5 * time.Second} - for _, path := range []string{"/token", "/jwt", "/"} { - resp, err := client.Get(baseURL + path) - if err != nil { - continue - } - body, _ := io.ReadAll(resp.Body) - resp.Body.Close() - if resp.StatusCode != 200 { - continue - } - var parsed struct { - Token string `json:"token"` - } - if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { - return parsed.Token, nil - } - s := strings.TrimSpace(string(body)) - if len(s) > 50 { - return s, nil - } - } - return "", fmt.Errorf("no working path on sidecar") -} - func mintCodexJWT(sessionCookie string) (string, error) { // CRITICAL: route through HTTP_PROXY / HTTPS_PROXY (Webshare). Direct // Railway IPs get stuck in Vercel's bot-ban loop after a few mints. diff --git a/harnesses/rpc-capabilities/cmd/script/probe.go b/harnesses/rpc-capabilities/cmd/script/probe.go index b27502c5..3856911e 100644 --- a/harnesses/rpc-capabilities/cmd/script/probe.go +++ b/harnesses/rpc-capabilities/cmd/script/probe.go @@ -122,7 +122,7 @@ func callLatestBlock(ctx context.Context, url string) (block uint64, hash string start := time.Now() resp, err := client.Do(req) - latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + latencyMs = float64(time.Since(start).Milliseconds()) if err != nil { if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { @@ -309,7 +309,7 @@ func callLatestSlot(ctx context.Context, url string) (slot uint64, result string start := time.Now() resp, err := client.Do(req) - latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + latencyMs = float64(time.Since(start).Milliseconds()) if err != nil { if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { @@ -375,7 +375,7 @@ func callSubstrateHeader(ctx context.Context, url string) (block uint64, hash st start := time.Now() resp, err := client.Do(req) - latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + latencyMs = float64(time.Since(start).Milliseconds()) if err != nil { if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { @@ -449,7 +449,7 @@ func callCosmosStatus(ctx context.Context, url string) (block uint64, hash strin start := time.Now() resp, err := client.Do(req) - latencyMs = float64(time.Since(start).Nanoseconds()) / 1e6 + latencyMs = float64(time.Since(start).Milliseconds()) if err != nil { if ctx.Err() != nil || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "Timeout") { diff --git a/harnesses/solana-dex-volume/Dockerfile b/harnesses/solana-dex-volume/Dockerfile deleted file mode 100644 index c620d261..00000000 --- a/harnesses/solana-dex-volume/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod go.sum ./ -COPY cmd ./cmd -RUN go mod download && CGO_ENABLED=0 GOOS=linux go build -o /app/harness ./cmd/script - -FROM debian:bookworm-slim -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/harness /app/harness -EXPOSE 2112 -CMD ["/app/harness"] diff --git a/harnesses/solana-dex-volume/cmd/script/main.go b/harnesses/solana-dex-volume/cmd/script/main.go deleted file mode 100644 index e04d2049..00000000 --- a/harnesses/solana-dex-volume/cmd/script/main.go +++ /dev/null @@ -1,175 +0,0 @@ -// solana-dex-volume -- Bench 205 -// -// Polls the DeFiLlama DEX API every 30 minutes for each tracked platform -// and exposes per-platform 24h/7d volume and protocol revenue as Prometheus gauges. -// -// No API key required. Endpoint: https://api.llama.fi/summary/dexs/{slug} -// -// Metrics on :2112/metrics: -// -// defillama_dex_volume_24h_usd{platform} -// defillama_dex_volume_7d_usd{platform} -// defillama_dex_fees_24h_usd{platform} (protocol revenue, ?dataType=dailyRevenue) -// defillama_dex_fees_7d_usd{platform} -// defillama_dex_take_rate{platform} (fees24h / volume24h) -// defillama_dex_health{platform} -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -const ( - pollInterval = 30 * time.Minute - baseURL = "https://api.llama.fi" -) - -var platforms = []struct { - slug string - label string -}{ - {"pump.fun", "pump-fun"}, - {"gmgn", "gmgn"}, - {"axiom", "axiom"}, - {"fomo-wallet", "fomo"}, - {"trojan", "trojan"}, - {"photon", "photon"}, - {"bullx", "bullx"}, -} - -var ( - volume24h = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "defillama_dex_volume_24h_usd", - Help: "24h DEX trading volume in USD from DeFiLlama", - }, []string{"platform"}) - - volume7d = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "defillama_dex_volume_7d_usd", - Help: "7-day DEX trading volume in USD from DeFiLlama", - }, []string{"platform"}) - - fees24h = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "defillama_dex_fees_24h_usd", - Help: "24h protocol revenue in USD from DeFiLlama (dailyRevenue dataType)", - }, []string{"platform"}) - - fees7d = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "defillama_dex_fees_7d_usd", - Help: "7-day protocol revenue in USD from DeFiLlama (dailyRevenue dataType)", - }, []string{"platform"}) - - takeRate = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "defillama_dex_take_rate", - Help: "Protocol revenue as fraction of trading volume (fees24h / volume24h)", - }, []string{"platform"}) - - health = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "defillama_dex_health", - Help: "1 if last DeFiLlama fetch succeeded, 0 otherwise", - }, []string{"platform"}) -) - -type llamaResponse struct { - Total24h float64 `json:"total24h"` - Total7d float64 `json:"total7d"` -} - -func fetch(endpoint, slug, query string) (llamaResponse, error) { - url := fmt.Sprintf("%s/summary/%s/%s", baseURL, endpoint, slug) - if query != "" { - url += "?" + query - } - resp, err := http.Get(url) - if err != nil { - return llamaResponse{}, err - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - return llamaResponse{}, fmt.Errorf("HTTP %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return llamaResponse{}, err - } - var r llamaResponse - if err := json.Unmarshal(body, &r); err != nil { - return llamaResponse{}, err - } - return r, nil -} - -func runOnce() { - for _, p := range platforms { - vol, err := fetch("dexs", p.slug, "") - if err != nil { - fmt.Printf("[poll] volume %s: %v\n", p.slug, err) - health.WithLabelValues(p.label).Set(0) - continue - } - - rev, err := fetch("fees", p.slug, "dataType=dailyRevenue") - if err != nil { - fmt.Printf("[poll] fees %s: %v (volume ok)\n", p.slug, err) - } - - volume24h.WithLabelValues(p.label).Set(vol.Total24h) - volume7d.WithLabelValues(p.label).Set(vol.Total7d) - fees24h.WithLabelValues(p.label).Set(rev.Total24h) - fees7d.WithLabelValues(p.label).Set(rev.Total7d) - if vol.Total24h > 0 { - takeRate.WithLabelValues(p.label).Set(rev.Total24h / vol.Total24h) - } - health.WithLabelValues(p.label).Set(1) - fmt.Printf("[poll] %s: vol24=$%.0f vol7d=$%.0f rev24=$%.0f rate=%.4f\n", - p.label, vol.Total24h, vol.Total7d, rev.Total24h, rev.Total24h/max(vol.Total24h, 1)) - } -} - -func max(a, b float64) float64 { - if a > b { - return a - } - return b -} - -func main() { - fmt.Println("=== solana-dex-volume harness (Bench 205) ===") - - go func() { - http.Handle("/metrics", promhttp.Handler()) - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) - fmt.Println("[srv] metrics on :2112") - if err := http.ListenAndServe(":2112", nil); err != nil { - fmt.Fprintf(os.Stderr, "[fatal] %v\n", err) - os.Exit(1) - } - }() - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - - runOnce() - - tick := time.NewTicker(pollInterval) - defer tick.Stop() - - for { - select { - case <-sig: - return - case <-tick.C: - runOnce() - } - } -} diff --git a/harnesses/solana-dex-volume/go.mod b/harnesses/solana-dex-volume/go.mod deleted file mode 100644 index 8655e4bf..00000000 --- a/harnesses/solana-dex-volume/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module solana-dex-volume - -go 1.24.0 - -require github.com/prometheus/client_golang v1.23.2 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.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 - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/sys v0.35.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect -) diff --git a/harnesses/solana-dex-volume/go.sum b/harnesses/solana-dex-volume/go.sum deleted file mode 100644 index d6b8ca98..00000000 --- a/harnesses/solana-dex-volume/go.sum +++ /dev/null @@ -1,46 +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -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/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= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/solana-exec/cmd/api/main.go b/harnesses/solana-exec/cmd/api/main.go index 79c3853a..3fcf7cac 100644 --- a/harnesses/solana-exec/cmd/api/main.go +++ b/harnesses/solana-exec/cmd/api/main.go @@ -3,13 +3,10 @@ package main import ( "context" "encoding/json" - "fmt" - "io" "log" "net/http" "os" "sort" - "sync" "time" "github.com/jackc/pgx/v5/pgxpool" @@ -29,8 +26,6 @@ func main() { w.Write([]byte(`{"status":"ok"}`)) }) mux.HandleFunc("/api/exec-leaderboard", corsJSON(handleExecLeaderboard(pool))) - mux.HandleFunc("/api/evm-revenue", corsJSON(handleEVMRevenue(pool))) - mux.HandleFunc("/metrics", handleMetrics(pool)) addr := ":2116" log.Printf("exec-api listening on %s", addr) @@ -46,7 +41,6 @@ type WindowStats struct { P50CUPriceMicro float64 `json:"p50CUPriceMicro"` P95CUPriceMicro float64 `json:"p95CUPriceMicro"` AvgPlatformFeeLamports float64 `json:"avgPlatformFeeLamports"` - SumPlatformFeeLamports int64 `json:"sumPlatformFeeLamports"` JitoRate float64 `json:"jitoRate"` AvgCUConsumed float64 `json:"avgCUConsumed"` } @@ -68,70 +62,48 @@ 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 - f.platform, + platform, -- weighted averages: SUM(avg×count)/SUM(count) avoids skewing by small off-peak buckets - 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, - COALESCE(SUM(f.sum_platform_fee_lamports) FILTER (WHERE f.bucket_start >= now() - INTERVAL '24 hours'), 0) AS h24_sum_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, - COALESCE(SUM(f.sum_platform_fee_lamports) FILTER (WHERE f.bucket_start >= now() - INTERVAL '7 days'), 0) AS d7_sum_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, - COALESCE(SUM(f.sum_platform_fee_lamports) FILTER (WHERE f.bucket_start >= now() - INTERVAL '30 days'), 0) AS d30_sum_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) - WHERE f.bucket_start >= now() - INTERVAL '31 days' - GROUP BY f.platform`, + 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`, ) if err != nil { log.Printf("exec-api: query: %v", err) @@ -144,20 +116,17 @@ func handleExecLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { for rows.Next() { var plt, latestBkt string var h24Prio, h24P50, h24P95, h24Pfee, h24Jito, h24Cu *float64 - var h24SumPfee int64 var h24Count *int64 var d7Prio, d7P50, d7P95, d7Pfee, d7Jito, d7Cu *float64 - var d7SumPfee int64 var d7Count *int64 var d30Prio, d30P50, d30P95, d30Pfee, d30Jito, d30Cu *float64 - var d30SumPfee int64 var d30Count *int64 if err := rows.Scan( &plt, - &h24Prio, &h24P50, &h24P95, &h24Pfee, &h24SumPfee, &h24Jito, &h24Cu, &h24Count, - &d7Prio, &d7P50, &d7P95, &d7Pfee, &d7SumPfee, &d7Jito, &d7Cu, &d7Count, - &d30Prio, &d30P50, &d30P95, &d30Pfee, &d30SumPfee, &d30Jito, &d30Cu, &d30Count, + &h24Prio, &h24P50, &h24P95, &h24Pfee, &h24Jito, &h24Cu, &h24Count, + &d7Prio, &d7P50, &d7P95, &d7Pfee, &d7Jito, &d7Cu, &d7Count, + &d30Prio, &d30P50, &d30P95, &d30Pfee, &d30Jito, &d30Cu, &d30Count, &latestBkt, ); err != nil { log.Printf("exec-api: scan: %v", err) @@ -168,9 +137,9 @@ func handleExecLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { Platform: plt, LatestBkt: latestBkt, Windows: map[string]WindowStats{ - "24h": buildWindow(h24Prio, h24P50, h24P95, h24Pfee, h24SumPfee, h24Jito, h24Cu, h24Count), - "7d": buildWindow(d7Prio, d7P50, d7P95, d7Pfee, d7SumPfee, d7Jito, d7Cu, d7Count), - "30d": buildWindow(d30Prio, d30P50, d30P95, d30Pfee, d30SumPfee, d30Jito, d30Cu, d30Count), + "24h": buildWindow(h24Prio, h24P50, h24P95, h24Pfee, h24Jito, h24Cu, h24Count), + "7d": buildWindow(d7Prio, d7P50, d7P95, d7Pfee, d7Jito, d7Cu, d7Count), + "30d": buildWindow(d30Prio, d30P50, d30P95, d30Pfee, d30Jito, d30Cu, d30Count), }, } platforms = append(platforms, p) @@ -190,7 +159,7 @@ func handleExecLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { } } -func buildWindow(prio, p50, p95, pfee *float64, sumPfee int64, jito, cu *float64, count *int64) WindowStats { +func buildWindow(prio, p50, p95, pfee, jito, cu *float64, count *int64) WindowStats { ws := WindowStats{} if count != nil { ws.TxCount = *count @@ -207,7 +176,6 @@ func buildWindow(prio, p50, p95, pfee *float64, sumPfee int64, jito, cu *float64 if pfee != nil { ws.AvgPlatformFeeLamports = *pfee } - ws.SumPlatformFeeLamports = sumPfee if jito != nil { ws.JitoRate = *jito } @@ -232,308 +200,3 @@ func mustEnv(key string) string { } return v } - -// ---- Prometheus /metrics endpoint ---- - -type latestBucketRow struct { - platform string - jitoRate float64 - p50CUPriceMicro float64 - p95CUPriceMicro float64 - avgPriorityFeeLamports float64 - avgPlatformFeeLamports float64 - txCount int64 -} - -func handleMetrics(pool *pgxpool.Pool) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - // Latest hourly bucket per platform. - latestRows, err := pool.Query(ctx, ` - SELECT DISTINCT ON (platform) - platform, jito_rate, p50_cu_price_micro, p95_cu_price_micro, - avg_priority_fee_lamports, avg_platform_fee_lamports, tx_count - FROM solana_exec_facts - ORDER BY platform, bucket_start DESC`) - if err != nil { - log.Printf("metrics: latest query: %v", err) - http.Error(w, "internal", http.StatusInternalServerError) - return - } - defer latestRows.Close() - - var latest []latestBucketRow - for latestRows.Next() { - var row latestBucketRow - if err := latestRows.Scan( - &row.platform, - &row.jitoRate, - &row.p50CUPriceMicro, - &row.p95CUPriceMicro, - &row.avgPriorityFeeLamports, - &row.avgPlatformFeeLamports, - &row.txCount, - ); err != nil { - log.Printf("metrics: scan: %v", err) - continue - } - latest = append(latest, row) - } - latestRows.Close() - - // 24h tx counts. - countRows, err := pool.Query(ctx, ` - SELECT platform, SUM(tx_count) AS count_24h - FROM solana_exec_facts - WHERE bucket_start >= now() - INTERVAL '24 hours' - GROUP BY platform`) - if err != nil { - log.Printf("metrics: count query: %v", err) - http.Error(w, "internal", http.StatusInternalServerError) - return - } - defer countRows.Close() - - counts24h := make(map[string]int64) - for countRows.Next() { - var plt string - var cnt int64 - if err := countRows.Scan(&plt, &cnt); err != nil { - continue - } - counts24h[plt] = cnt - } - countRows.Close() - - w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8") - - fmt.Fprint(w, "# HELP solana_exec_jito_rate Fraction of transactions with a Jito tip, latest hourly bucket\n") - fmt.Fprint(w, "# TYPE solana_exec_jito_rate gauge\n") - for _, row := range latest { - fmt.Fprintf(w, "solana_exec_jito_rate{platform=%q} %g\n", row.platform, row.jitoRate) - } - - fmt.Fprint(w, "# HELP solana_exec_cu_price_p50_micro Median compute unit price microlamports/CU, latest hourly bucket\n") - fmt.Fprint(w, "# TYPE solana_exec_cu_price_p50_micro gauge\n") - for _, row := range latest { - fmt.Fprintf(w, "solana_exec_cu_price_p50_micro{platform=%q} %g\n", row.platform, row.p50CUPriceMicro) - } - - fmt.Fprint(w, "# HELP solana_exec_cu_price_p95_micro p95 compute unit price microlamports/CU, latest hourly bucket\n") - fmt.Fprint(w, "# TYPE solana_exec_cu_price_p95_micro gauge\n") - for _, row := range latest { - fmt.Fprintf(w, "solana_exec_cu_price_p95_micro{platform=%q} %g\n", row.platform, row.p95CUPriceMicro) - } - - fmt.Fprint(w, "# HELP solana_exec_priority_fee_lamports Average priority fee in lamports, latest hourly bucket\n") - fmt.Fprint(w, "# TYPE solana_exec_priority_fee_lamports gauge\n") - for _, row := range latest { - fmt.Fprintf(w, "solana_exec_priority_fee_lamports{platform=%q} %g\n", row.platform, row.avgPriorityFeeLamports) - } - - fmt.Fprint(w, "# HELP solana_exec_platform_fee_lamports Average platform fee in lamports, latest hourly bucket\n") - fmt.Fprint(w, "# TYPE solana_exec_platform_fee_lamports gauge\n") - for _, row := range latest { - fmt.Fprintf(w, "solana_exec_platform_fee_lamports{platform=%q} %g\n", row.platform, row.avgPlatformFeeLamports) - } - - fmt.Fprint(w, "# HELP solana_exec_tx_count_24h Total transaction count over the last 24 hours\n") - fmt.Fprint(w, "# TYPE solana_exec_tx_count_24h gauge\n") - for plt, cnt := range counts24h { - fmt.Fprintf(w, "solana_exec_tx_count_24h{platform=%q} %d\n", plt, cnt) - } - } -} - -// ---- EVM revenue endpoint ---- - -// evmCoverage is the static coverage map derived from the evm-exec platform config. -// "full" = native + stable; "stable-only" = only ERC-20 USDC/USDG tracked. -// Mirrors platform.Coverage() in evm-exec/internal/platform/platforms.go. -var evmCoverage = map[string]map[string]string{ - "pumpfun": {"ethereum": "full", "bsc": "full", "base": "full"}, - "gmgn": {"ethereum": "full", "bsc": "full", "base": "full"}, - "maestro": {"ethereum": "full", "bsc": "full", "base": "full"}, - "axiom": {"bsc": "full"}, - "gmgn-robinhood": {"robinhood": "full"}, - "maestro-robinhood": {"robinhood": "full"}, - // banana-gun: evmKey=null in frontend config; EVM data intentionally not displayed - // (router addresses receive trade principal, not fees; pending eth_getLogs on topic 0x72015ace…) -} - -// coinGeckoIDs maps chain name → CoinGecko asset ID for native price lookup. -var coinGeckoIDs = map[string]string{ - "ethereum": "ethereum", - "bsc": "binancecoin", - "base": "ethereum", - "robinhood": "ethereum", -} - -// nativeSymbols maps chain → native asset ticker. -var nativeSymbols = map[string]string{ - "ethereum": "ETH", - "bsc": "BNB", - "base": "ETH", - "robinhood": "ETH", -} - -type priceCache struct { - mu sync.Mutex - prices map[string]float64 // coingecko id → USD price - fetchedAt time.Time -} - -var globalPrices = &priceCache{prices: make(map[string]float64)} - -func (p *priceCache) get(ctx context.Context) map[string]float64 { - p.mu.Lock() - defer p.mu.Unlock() - if time.Since(p.fetchedAt) < 15*time.Minute && len(p.prices) > 0 { - out := make(map[string]float64, len(p.prices)) - for k, v := range p.prices { - out[k] = v - } - return out - } - reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, - "https://api.coingecko.com/api/v3/simple/price?ids=ethereum,binancecoin&vs_currencies=usd", nil) - if err != nil { - return nil - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var raw map[string]map[string]float64 - if err := json.Unmarshal(body, &raw); err != nil { - return nil - } - for id, vs := range raw { - if usd, ok := vs["usd"]; ok { - p.prices[id] = usd - } - } - p.fetchedAt = time.Now() - out := make(map[string]float64, len(p.prices)) - for k, v := range p.prices { - out[k] = v - } - return out -} - -type NativeRevenue struct { - Symbol string `json:"symbol"` - Amount float64 `json:"amount"` - USD *float64 `json:"usd"` -} - -type EVMChainRevenue struct { - Stable24h float64 `json:"stable24h"` - Stable7d float64 `json:"stable7d"` - Stable30d float64 `json:"stable30d"` - Native *NativeRevenue `json:"native"` // 24h only; historical prices not stored - Coverage string `json:"coverage"` -} - -type EVMPlatformRow struct { - Platform string `json:"platform"` - Chains map[string]EVMChainRevenue `json:"chains"` -} - -type EVMRevenueResponse struct { - UpdatedAt string `json:"updatedAt"` - Platforms []EVMPlatformRow `json:"platforms"` -} - -func handleEVMRevenue(pool *pgxpool.Pool) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - rows, err := pool.Query(ctx, ` - SELECT chain, platform, - COALESCE(SUM(revenue_stable) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0), - COALESCE(SUM(revenue_stable) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days'), 0), - COALESCE(SUM(revenue_stable) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days'), 0), - COALESCE(SUM(revenue_native) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours'), 0), - COALESCE(MAX(native_symbol), '') - FROM evm_exec_facts - WHERE bucket_start >= now() - INTERVAL '30 days' - GROUP BY chain, platform`) - if err != nil { - log.Printf("evm-revenue: query: %v", err) - http.Error(w, "internal", http.StatusInternalServerError) - return - } - defer rows.Close() - - prices := globalPrices.get(r.Context()) - - // Group by platform. - byPlatform := make(map[string]map[string]EVMChainRevenue) - for rows.Next() { - var chain, plt, nativeSym string - var stable24h, stable7d, stable30d, native float64 - if err := rows.Scan(&chain, &plt, &stable24h, &stable7d, &stable30d, &native, &nativeSym); err != nil { - continue - } - if byPlatform[plt] == nil { - byPlatform[plt] = make(map[string]EVMChainRevenue) - } - - coverage := "stable-only" - if m, ok := evmCoverage[plt]; ok { - if c, ok := m[chain]; ok { - coverage = c - } - } - - var nat *NativeRevenue - if native > 0 || coverage == "full" { - sym := nativeSym - if sym == "" { - sym = nativeSymbols[chain] - } - nr := &NativeRevenue{Symbol: sym, Amount: native} - if cgID := coinGeckoIDs[chain]; cgID != "" && prices != nil { - if price, ok := prices[cgID]; ok { - usd := native * price - nr.USD = &usd - } - } - nat = nr - } - - byPlatform[plt][chain] = EVMChainRevenue{ - Stable24h: stable24h, - Stable7d: stable7d, - Stable30d: stable30d, - Native: nat, - Coverage: coverage, - } - } - if err := rows.Err(); err != nil { - log.Printf("evm-revenue: rows: %v", err) - } - - var platforms []EVMPlatformRow - for plt, chains := range byPlatform { - platforms = append(platforms, EVMPlatformRow{Platform: plt, Chains: chains}) - } - sort.Slice(platforms, func(i, j int) bool { - return platforms[i].Platform < platforms[j].Platform - }) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(EVMRevenueResponse{ - UpdatedAt: time.Now().UTC().Format(time.RFC3339), - Platforms: platforms, - }) - } -} diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go index 56cf537a..909c7774 100644 --- a/harnesses/solana-exec/cmd/collector/main.go +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -5,8 +5,6 @@ import ( "fmt" "log" "os" - "sort" - "strings" "time" "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/helius" @@ -26,17 +24,13 @@ func main() { } defer db.Close() - rpcURL := os.Getenv("SOLANA_RPC_URL") - if rpcURL == "" { - rpcURL = "https://api.mainnet-beta.solana.com" - } - h := helius.New(rpcURL) + h := helius.New(mustEnv("HELIUS_API_KEY")) log.Printf("collector: monitoring %d platforms", len(platform.FeeAccounts)) for { - for plt, feeAccounts := range platform.FeeAccounts { - if err := collect(ctx, db, h, plt, feeAccounts); err != nil { + for plt, feeAccount := range platform.FeeAccounts { + if err := collect(ctx, db, h, plt, feeAccount); err != nil { log.Printf("collector: %s: %v", plt, err) } } @@ -44,130 +38,76 @@ func main() { } } -type acctData struct { - cursorKey string - cursor store.Cursor - sigs []helius.SigEntry // newest-first from RPC -} - -func collect(ctx context.Context, db *store.DB, h *helius.Client, plt string, feeAccounts []string) error { - // SPL token mint for fee detection; empty = SOL (native lamports). - feeToken := platform.FeeToken[plt] - - // feeSet is used to detect fee transfers in NativeTransfers / postTokenBalances.owner. - // For SPL-token platforms (e.g. FOMO), FeeAccounts holds the ATA (for sig discovery) - // but postTokenBalances.owner is the wallet — so use FeeOwners when present. - feeOwners := platform.FeeOwners[plt] - if feeOwners == nil { - feeOwners = feeAccounts - } - feeSet := make(map[string]bool, len(feeOwners)) - for _, fa := range feeOwners { - feeSet[fa] = true +func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccount string) error { + cursor, err := db.GetCursor(ctx, plt) + if err != nil { + return fmt.Errorf("get cursor: %w", err) } - // Paginate each fee account independently with its own cursor so a crash - // between accounts doesn't cause re-processing of already-committed data. - perAcct := make([]acctData, 0, len(feeAccounts)) - - for _, feeAccount := range feeAccounts { - cursorKey := plt + ":" + feeAccount - cursor, err := db.GetCursor(ctx, cursorKey) + 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. + var sigs []helius.SigEntry + before := "" + for { + batch, err := h.GetSignaturesForAddress(ctx, feeAccount, sigLimit, cursor.LastSig, before) if err != nil { - return fmt.Errorf("get cursor %s: %w", feeAccount, err) + return fmt.Errorf("get sigs (before=%s): %w", before, err) } - - const sigLimit = 1000 - const maxPages = 300 // cap backfill at 300k sigs per account (~20 days for busy wallets) - var sigs []helius.SigEntry - before := "" - for page := 0; page < maxPages; page++ { - batch, err := h.GetSignaturesForAddress(ctx, feeAccount, sigLimit, cursor.LastSig, before) - if err != nil { - return fmt.Errorf("get sigs %s (before=%s): %w", feeAccount, before, err) - } - sigs = append(sigs, batch...) - if len(batch) < sigLimit { - break - } - before = batch[len(batch)-1].Signature - time.Sleep(100 * time.Millisecond) + sigs = append(sigs, batch...) + if len(batch) < sigLimit { + break // last page } - if len(sigs) > sigLimit { - log.Printf("collector: %s: %s: paginated %d sigs", plt, feeAccount, len(sigs)) - } - - perAcct = append(perAcct, acctData{cursorKey: cursorKey, cursor: cursor, sigs: sigs}) + 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 + } + if len(sigs) > sigLimit { + log.Printf("collector: %s: paginated %d raw sigs (%d pages)", plt, len(sigs), (len(sigs)+sigLimit-1)/sigLimit) } - // Merge sigs from all accounts, de-duplicate by signature. - // A single tx can touch multiple fee accounts of the same platform (rare but possible). - seen := make(map[string]struct{}) - var allSigs []helius.SigEntry - for _, acct := range perAcct { - for _, s := range acct.sigs { - if _, dup := seen[s.Signature]; dup { - continue - } - seen[s.Signature] = struct{}{} - allSigs = append(allSigs, s) - } + // Reverse to process oldest-first so cursor is always the true watermark. + reversed := make([]helius.SigEntry, len(sigs)) + for i, s := range sigs { + reversed[len(sigs)-1-i] = s } - if len(allSigs) == 0 { - return nil + // Pre-filter failed txs to avoid wasting enhanced-API credits. + // Failed txs don't generate platform fees; priority-fee stats should reflect + // the cost of successful trades only. + var sigStrs []string + for _, s := range reversed { + if s.Err == nil { + sigStrs = append(sigStrs, s.Signature) + } } - // Count raw hourly volume from the merged+deduped sig set so a tx touching - // multiple fee accounts of the same platform is counted exactly once. - // from_cursor is a compound of all account cursors for idempotency on retry. - fromCursor := compoundCursor(perAcct) + // Count successful sigs per hour bucket from raw pagination (full volume, no enhanced API cost). hourBuckets := make(map[time.Time]int64) - for _, s := range allSigs { + 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, fromCursor, hourBuckets); err != nil { + if err := db.UpsertRawCounts(ctx, plt, hourBuckets); err != nil { return fmt.Errorf("upsert raw counts: %w", err) } - // Pre-filter failed txs; only successful trades generate platform fees. - var sigStrs []string - for _, s := range allSigs { - if s.Err == nil { - sigStrs = append(sigStrs, s.Signature) - } - } - - // All sigs failed: still save cursors so we don't re-scan next poll. if len(sigStrs) == 0 { - return saveCursors(ctx, db, perAcct) - } - - // Sort by slot descending before striding so the sample is temporally uniform - // across multi-account platforms (e.g. Axiom 20 accounts — merge order is arbitrary). - sort.Slice(allSigs, func(i, j int) bool { return allSigs[i].Slot > allSigs[j].Slot }) - - // Rebuild sigStrs from the sorted order after filtering failed txs. - sigStrs = sigStrs[:0] - for _, s := range allSigs { - if s.Err == nil { - sigStrs = append(sigStrs, s.Signature) - } + // 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; stride-sample across the full window so the - // sample represents the whole poll period, not just the most recent transactions. + // 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 { - 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 + sigStrs = sigStrs[:100] } txs, err := h.GetEnhancedTransactions(ctx, sigStrs) @@ -188,52 +128,20 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt string, fe var platformFeeLamports, jitoTipLamports int64 isJito := false - - if feeToken == "" { - // SOL-fee platform: detect via native SOL transfers. - // Skip any tx where a fee wallet is also a sender — these are internal sweeps - // (e.g. Axiom consolidating 20 wallets) that would inflate the average fee. - hasFeeWalletSender := false - for _, sender := range tx.SenderAccounts { - if feeSet[sender] { - hasFeeWalletSender = true - break - } - } - if !hasFeeWalletSender { - for _, xfer := range tx.NativeTransfers { - if feeSet[xfer.ToUserAccount] { - platformFeeLamports += xfer.Amount - } - } - } - for _, xfer := range tx.NativeTransfers { - if platform.JitoTipAccounts[xfer.ToUserAccount] { - jitoTipLamports += xfer.Amount - isJito = true - } + for _, xfer := range tx.NativeTransfers { + if xfer.ToUserAccount == feeAccount { + platformFeeLamports += xfer.Amount } - } else { - // SPL-token-fee platform (e.g. FOMO uses USDC): detect via token transfers. - // platformFeeLamports stores raw token units (1 USDC = 1_000_000). - for _, xfer := range tx.TokenTransfers { - if feeSet[xfer.ToOwner] && xfer.Mint == feeToken { - platformFeeLamports += xfer.Amount - } - } - // Jito detection is SOL-based regardless of fee token. - for _, xfer := range tx.NativeTransfers { - if platform.JitoTipAccounts[xfer.ToUserAccount] { - jitoTipLamports += xfer.Amount - isJito = true - } + if platform.JitoTipAccounts[xfer.ToUserAccount] { + jitoTipLamports += xfer.Amount + isJito = true } } - // CU price comes directly from SetComputeUnitPrice (disc 0x03), already in - // micro-lamports per CU — no division needed. Only set when the instruction - // is present; 0 means base fee only, not a zero-price priority tx. - cuPriceMicro := tx.CUPriceDeclared + var cuPriceMicro int64 + if tx.ComputeUnitsConsumed > 0 && priorityFee > 0 { + cuPriceMicro = priorityFee * 1_000_000 / tx.ComputeUnitsConsumed + } events = append(events, store.ExecEvent{ Sig: tx.Signature, @@ -254,48 +162,13 @@ func collect(ctx context.Context, db *store.DB, h *helius.Client, plt string, fe return fmt.Errorf("upsert: %w", err) } - cuSamples := make([]store.CUSample, 0, len(events)) - for _, e := range events { - if e.CUPriceMicro > 0 { - cuSamples = append(cuSamples, store.CUSample{Sig: e.Sig, BlockTime: e.BlockTime, CUPriceMicro: e.CUPriceMicro}) - } - } - if err := db.InsertCUSamples(ctx, plt, cuSamples); err != nil { - return fmt.Errorf("insert cu samples: %w", err) - } - - if err := saveCursors(ctx, db, perAcct); err != nil { - return err - } - - log.Printf("collector: %s: ingested %d events", plt, len(events)) - return nil -} - -// compoundCursor returns a deterministic string that encodes the cursor state for -// all accounts in this poll, used as the from_cursor idempotency key in raw_counts. -func compoundCursor(perAcct []acctData) string { - if len(perAcct) == 0 { - return "" - } - parts := make([]string, 0, len(perAcct)) - for _, a := range perAcct { - parts = append(parts, a.cursorKey+"="+a.cursor.LastSig) + // 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 { + return fmt.Errorf("save cursor: %w", err) } - return strings.Join(parts, ",") -} -// saveCursors advances each fee account's cursor to its newest observed sig. -func saveCursors(ctx context.Context, db *store.DB, perAcct []acctData) error { - for _, acct := range perAcct { - if len(acct.sigs) == 0 { - continue - } - newest := acct.sigs[0] // RPC returns newest-first - if err := db.SaveCursor(ctx, acct.cursorKey, newest.Signature, newest.Slot); err != nil { - return fmt.Errorf("save cursor %s: %w", acct.cursorKey, err) - } - } + log.Printf("collector: %s: ingested %d txs (newest slot %d)", plt, len(events), newest.Slot) return nil } diff --git a/harnesses/solana-exec/cmd/materializer/main.go b/harnesses/solana-exec/cmd/materializer/main.go index 63e0c462..98fc753a 100644 --- a/harnesses/solana-exec/cmd/materializer/main.go +++ b/harnesses/solana-exec/cmd/materializer/main.go @@ -27,12 +27,6 @@ func main() { log.Printf("materializer: %s: done", plt) } } - 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/docker-compose.solana-exec.yml b/harnesses/solana-exec/docker-compose.solana-exec.yml index 7a956401..63d252d0 100644 --- a/harnesses/solana-exec/docker-compose.solana-exec.yml +++ b/harnesses/solana-exec/docker-compose.solana-exec.yml @@ -1,6 +1,5 @@ # Fragment to merge into /opt/ocb/docker-compose.yml on VPS. -# Requires: ocb-postgres (from docker-compose.apps.yml). -# Optional: SOLANA_RPC_URL (defaults to api.mainnet-beta.solana.com). +# Requires: ocb-postgres (from docker-compose.apps.yml), HELIUS_API_KEY in env. services: solana-exec-collector: diff --git a/harnesses/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go index 58b0743a..45bc6c17 100644 --- a/harnesses/solana-exec/internal/helius/client.go +++ b/harnesses/solana-exec/internal/helius/client.go @@ -3,37 +3,29 @@ package helius import ( "bytes" "context" - "encoding/binary" "encoding/json" "fmt" - "math/big" "net/http" - "strconv" - "strings" - "sync" "time" ) -// Client wraps the Solana JSON-RPC API. -// All calls use standard Solana JSON-RPC — no third-party credits required. +// Client wraps the Helius RPC + enhanced-transactions APIs. type Client struct { httpClient *http.Client - rpcURL string + apiKey string + rpcURL string // https://mainnet.helius-rpc.com/?api-key=KEY + enhURL string // https://api.helius.xyz/v0/transactions?api-key=KEY } -// New creates a client using the given Solana RPC endpoint. -func New(rpcURL string) *Client { +func New(apiKey string) *Client { return &Client{ httpClient: &http.Client{Timeout: 30 * time.Second}, - rpcURL: rpcURL, + apiKey: apiKey, + rpcURL: fmt.Sprintf("https://mainnet.helius-rpc.com/?api-key=%s", apiKey), + enhURL: fmt.Sprintf("https://api.helius.xyz/v0/transactions?api-key=%s", apiKey), } } -// NewWithRPC kept for call-site compat; apiKey is ignored. -func NewWithRPC(_, rpcURL string) *Client { - return New(rpcURL) -} - // SigEntry is one result from getSignaturesForAddress. type SigEntry struct { Signature string `json:"signature"` @@ -44,7 +36,8 @@ type SigEntry struct { // GetSignaturesForAddress fetches up to `limit` finalized signatures for // `address`, newest-first. `until` is the exclusive upper bound (cursor); -// `before` is the exclusive lower bound for pagination (pass "" for first page). +// `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, @@ -64,412 +57,90 @@ func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, li "params": []any{address, params}, }) - 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") - 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() - resp, err := c.httpClient.Do(req) - if err != nil { - lastErr = err - continue - } - if resp.StatusCode == http.StatusTooManyRequests { - resp.Body.Close() - lastErr = fmt.Errorf("RPC HTTP 429") - continue - } - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - return nil, fmt.Errorf("RPC HTTP %d", resp.StatusCode) - } + 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 { - resp.Body.Close() - return nil, fmt.Errorf("RPC decode: %w", err) - } - resp.Body.Close() - if out.Error != nil { - return nil, fmt.Errorf("RPC error %d: %s", out.Error.Code, out.Error.Message) - } - return out.Result, nil + var out struct { + Result []SigEntry `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` } - return nil, fmt.Errorf("RPC: %w (after 3 attempts)", lastErr) + 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 } -// NativeTransfer is a SOL transfer derived from pre/post balance diff. +// NativeTransfer is a SOL transfer extracted by Helius. type NativeTransfer struct { - FromUserAccount string - ToUserAccount string - Amount int64 // lamports -} - -// TokenTransfer is an SPL token transfer derived from pre/post token balance diff. -// Amount is in the token's raw units (e.g. 1 USDC = 1_000_000). -type TokenTransfer struct { - ToOwner string // wallet that owns the destination token account - Mint string // SPL token mint address - Amount int64 // raw token units (positive = received) + FromUserAccount string `json:"fromUserAccount"` + ToUserAccount string `json:"toUserAccount"` + Amount int64 `json:"amount"` // lamports } -// EnhancedTx is a parsed Solana transaction. +// EnhancedTx is the Helius-parsed transaction. type EnhancedTx struct { - Signature string - Slot uint64 - Timestamp int64 // unix seconds - Fee int64 // total fee lamports - FeePayer string - NativeTransfers []NativeTransfer - TokenTransfers []TokenTransfer - TransactionError any - ComputeUnitsConsumed int64 - // CULimit from SetComputeUnitLimit (disc 0x02). 0 = instruction absent. - CULimit int64 - // CUPriceDeclared is the micro-lamports/CU from SetComputeUnitPrice (disc 0x03). - // This is the authoritative price — use it directly, no division needed. - // 0 = no priority fee set (base fee only). - CUPriceDeclared int64 - // SenderAccounts is the set of accounts whose SOL balance decreased in this tx. - // Used by the collector to detect internal fee-wallet sweeps and skip them. - SenderAccounts []string + Signature string `json:"signature"` + Slot uint64 `json:"slot"` + Timestamp int64 `json:"timestamp"` // unix seconds + Fee int64 `json:"fee"` // total fee lamports + FeePayer string `json:"feePayer"` + NativeTransfers []NativeTransfer `json:"nativeTransfers"` + TransactionError any `json:"transactionError"` + ComputeUnitsConsumed int64 `json:"computeUnitsConsumed"` } -const b58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - -// decodeBase58 decodes a base58-encoded Solana instruction data string to bytes. -func decodeBase58(s string) []byte { - var n big.Int - for _, c := range s { - idx := strings.IndexRune(b58Alphabet, c) - if idx < 0 { - return nil - } - n.Mul(&n, big.NewInt(58)) - n.Add(&n, big.NewInt(int64(idx))) - } - decoded := n.Bytes() - leading := 0 - for _, c := range s { - if c != '1' { - break - } - leading++ - } - out := make([]byte, leading+len(decoded)) - copy(out[leading:], decoded) - return out -} - -const computeBudgetProgram = "ComputeBudget111111111111111111111111111111" - -// GetEnhancedTransactions fetches and parses up to 100 transactions in parallel -// using standard Solana JSON-RPC getTransaction — no third-party credits needed. -// NativeTransfers and TokenTransfers are derived from pre/post balance diffs. +// GetEnhancedTransactions fetches enhanced parsed transactions for up to 100 +// signatures in a single request. Helius caps the batch at 100. func (c *Client) GetEnhancedTransactions(ctx context.Context, sigs []string) ([]EnhancedTx, error) { if len(sigs) == 0 { return nil, nil } if len(sigs) > 100 { - return nil, fmt.Errorf("batch max 100 sigs, got %d", len(sigs)) - } - - type result struct { - tx EnhancedTx - ok bool - } - results := make([]result, len(sigs)) - - sem := make(chan struct{}, 10) - var wg sync.WaitGroup - for i, sig := range sigs { - i, sig := i, sig - wg.Add(1) - go func() { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() - tx, err := c.getTransaction(ctx, sig) - if err != nil { - return - } - results[i] = result{tx: tx, ok: true} - }() - } - wg.Wait() - - txs := make([]EnhancedTx, 0, len(sigs)) - for _, r := range results { - if r.ok { - txs = append(txs, r.tx) - } + return nil, fmt.Errorf("helius: enhanced batch max 100 sigs, got %d", len(sigs)) } - return txs, nil -} - -// accountKey handles both legacy (bare string) and v0 (object with pubkey field) formats. -type accountKey struct{ Pubkey string } -func (a *accountKey) UnmarshalJSON(b []byte) error { - var s string - if json.Unmarshal(b, &s) == nil { - a.Pubkey = s - return nil - } - var obj struct { - Pubkey string `json:"pubkey"` - } - if err := json.Unmarshal(b, &obj); err != nil { - return err - } - a.Pubkey = obj.Pubkey - return nil -} - -func (c *Client) getTransaction(ctx context.Context, sig string) (EnhancedTx, error) { body, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", - "id": 1, - "method": "getTransaction", - "params": []any{sig, map[string]any{ - "encoding": "json", - "commitment": "finalized", - "maxSupportedTransactionVersion": 0, - }}, + "transactions": sigs, }) - type tokenBalanceEntry struct { - AccountIndex int `json:"accountIndex"` - Mint string `json:"mint"` - Owner string `json:"owner"` - UITokenAmount struct { - Amount string `json:"amount"` // raw integer as string - } `json:"uiTokenAmount"` - } - - var out struct { - Result *struct { - Slot uint64 `json:"slot"` - BlockTime *int64 `json:"blockTime"` - Transaction struct { - Message struct { - AccountKeys []accountKey `json:"accountKeys"` - Instructions []struct { - ProgramIDIndex int `json:"programIdIndex"` - Data string `json:"data"` // base58-encoded - } `json:"instructions"` - } `json:"message"` - Signatures []string `json:"signatures"` - } `json:"transaction"` - Meta *struct { - Err any `json:"err"` - Fee int64 `json:"fee"` - PreBalances []int64 `json:"preBalances"` - PostBalances []int64 `json:"postBalances"` - ComputeUnitsConsumed *int64 `json:"computeUnitsConsumed"` - PreTokenBalances []tokenBalanceEntry `json:"preTokenBalances"` - PostTokenBalances []tokenBalanceEntry `json:"postTokenBalances"` - LoadedAddresses *struct { - Writable []string `json:"writable"` - Readonly []string `json:"readonly"` - } `json:"loadedAddresses"` - } `json:"meta"` - } `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - - var lastErr error - for attempt := range 3 { - if attempt > 0 { - select { - case <-ctx.Done(): - return EnhancedTx{}, 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 EnhancedTx{}, 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("getTransaction HTTP 429") - continue - } - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - return EnhancedTx{}, fmt.Errorf("getTransaction HTTP %d", resp.StatusCode) - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - resp.Body.Close() - lastErr = fmt.Errorf("decode: %w", err) - continue - } - resp.Body.Close() - lastErr = nil - break - } - if lastErr != nil { - return EnhancedTx{}, fmt.Errorf("getTransaction: %w (after 3 attempts)", lastErr) - } - if out.Error != nil { - return EnhancedTx{}, fmt.Errorf("RPC error %d: %s", out.Error.Code, out.Error.Message) - } - if out.Result == nil || out.Result.Meta == nil { - return EnhancedTx{}, fmt.Errorf("null result for %s", sig) - } - - r := out.Result - m := r.Meta - - // Full account key list: static keys + v0 loaded addresses (writable then readonly). - accounts := make([]accountKey, len(r.Transaction.Message.AccountKeys)) - copy(accounts, r.Transaction.Message.AccountKeys) - if m.LoadedAddresses != nil { - for _, addr := range m.LoadedAddresses.Writable { - accounts = append(accounts, accountKey{Pubkey: addr}) - } - for _, addr := range m.LoadedAddresses.Readonly { - accounts = append(accounts, accountKey{Pubkey: addr}) - } - } - - // NativeTransfers: accounts whose SOL balance increased received SOL. - // SenderAccounts: accounts whose SOL balance decreased (used for sweep detection). - var nativeTransfers []NativeTransfer - var senderAccounts []string - for i, key := range accounts { - if i >= len(m.PreBalances) || i >= len(m.PostBalances) { - break - } - diff := m.PostBalances[i] - m.PreBalances[i] - if diff > 0 { - nativeTransfers = append(nativeTransfers, NativeTransfer{ToUserAccount: key.Pubkey, Amount: diff}) - } else if diff < 0 { - senderAccounts = append(senderAccounts, key.Pubkey) - } - } - - // TokenTransfers: net SPL token gain per (owner, mint). - // Sum pre and post separately so wallets with multiple ATAs for the same mint - // are handled correctly — the diff is the net change, not a per-account delta. - type ownerMint struct{ owner, mint string } - preTokenAmt := make(map[ownerMint]int64, len(m.PreTokenBalances)) - for _, tb := range m.PreTokenBalances { - if tb.Owner == "" { - continue - } - amt, _ := strconv.ParseInt(tb.UITokenAmount.Amount, 10, 64) - preTokenAmt[ownerMint{tb.Owner, tb.Mint}] += amt - } - postTokenAmt := make(map[ownerMint]int64, len(m.PostTokenBalances)) - for _, tb := range m.PostTokenBalances { - if tb.Owner == "" { - continue // owner field absent on very old txs - } - amt, _ := strconv.ParseInt(tb.UITokenAmount.Amount, 10, 64) - postTokenAmt[ownerMint{tb.Owner, tb.Mint}] += amt - } - var tokenTransfers []TokenTransfer - for key, postAmt := range postTokenAmt { - diff := postAmt - preTokenAmt[key] - if diff > 0 { - tokenTransfers = append(tokenTransfers, TokenTransfer{ - ToOwner: key.owner, - Mint: key.mint, - Amount: diff, - }) - } - } - - var cuConsumed int64 - if m.ComputeUnitsConsumed != nil { - cuConsumed = *m.ComputeUnitsConsumed + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.enhURL, bytes.NewReader(body)) + if err != nil { + return nil, err } + req.Header.Set("Content-Type", "application/json") - // Parse ComputeBudget instructions for CU price and limit. - // Discriminator enum (borsh u8): - // 0x02 = SetComputeUnitLimit payload: u32 LE [1:5] - // 0x03 = SetComputeUnitPrice payload: u64 LE [1:9] (micro-lamports per CU) - var cuLimit, cuPriceDeclared int64 - for _, ix := range r.Transaction.Message.Instructions { - if ix.ProgramIDIndex < 0 || ix.ProgramIDIndex >= len(accounts) { - continue - } - if accounts[ix.ProgramIDIndex].Pubkey != computeBudgetProgram { - continue - } - data := decodeBase58(ix.Data) - if len(data) == 0 { - continue - } - switch data[0] { - case 2: - if len(data) >= 5 { - cuLimit = int64(binary.LittleEndian.Uint32(data[1:5])) - } - case 3: - if len(data) >= 9 { - cuPriceDeclared = int64(binary.LittleEndian.Uint64(data[1:9])) - } - } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err } + defer resp.Body.Close() - var feePayer string - if len(accounts) > 0 { - feePayer = accounts[0].Pubkey + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("helius enhanced HTTP %d", resp.StatusCode) } - var blockTime int64 - if r.BlockTime != nil { - blockTime = *r.BlockTime + var txs []EnhancedTx + if err := json.NewDecoder(resp.Body).Decode(&txs); err != nil { + return nil, fmt.Errorf("helius enhanced decode: %w", err) } - - var sig0 string - if len(r.Transaction.Signatures) > 0 { - sig0 = r.Transaction.Signatures[0] - } - - return EnhancedTx{ - Signature: sig0, - Slot: r.Slot, - Timestamp: blockTime, - Fee: m.Fee, - FeePayer: feePayer, - NativeTransfers: nativeTransfers, - TokenTransfers: tokenTransfers, - TransactionError: m.Err, - ComputeUnitsConsumed: cuConsumed, - CULimit: cuLimit, - CUPriceDeclared: cuPriceDeclared, - SenderAccounts: senderAccounts, - }, nil + return txs, nil } diff --git a/harnesses/solana-exec/internal/platform/platforms.go b/harnesses/solana-exec/internal/platform/platforms.go index a1ec8b7a..16864d1d 100644 --- a/harnesses/solana-exec/internal/platform/platforms.go +++ b/harnesses/solana-exec/internal/platform/platforms.go @@ -1,106 +1,13 @@ package platform -// FeeAccounts maps platform name → its on-chain fee-receiver accounts. -// Multi-account platforms (GMGN, Axiom) list all wallets; the collector -// paginates each independently and aggregates before sampling. -// Sources: DefiLlama dimension-adapters, verified 2026-08. -var FeeAccounts = map[string][]string{ - // 8 bonding-curve fee recipients (round-robin) + Mayhem wallet. - // Sources: DeFiLlama fees/pumpdotfun adapter (master, 2026-08). - "pump.fun": { - "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", - "62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV", - "FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz", - "7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX", - "AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY", - "9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz", - "G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP", - "7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ", - "GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS", // Mayhem mode - }, - "photon": { - "AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH", - }, - "bullx": { - "9RYJ3qr5eU5xAooqVcbmdeusjcViL5Nkiq7Gske3tiKq", // current (post 2024-11-16) - "F4hJ3Ee3c5UuaorKAMfELBjYCjiiLH75haZTKqTywRP3", // legacy - }, - "gmgn": { - // Nine pure fee-collector wallets verified on-chain (2026-06). - // No transfers among themselves → positive SOL diff = fee, no double-count. - "BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT", - "7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA", - "HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3", - "ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy", - "DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9", - "3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK", - "DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx", - "dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ", - "6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn", - }, - "axiom": { - "7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj", - "4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S", - "CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ", - "AaG6of1gbj1pbDumvbSiTuJhRCRkkUNaWVxijSbWvTJW", - "7oi1L8U9MRu5zDz5syFahsiLUric47LzvJBQX6r827ws", - "9kPrgLggBJ69tx1czYAbp7fezuUmL337BsqQTKETUEhP", - "DKyUs1xXMDy8Z11zNsLnUg3dy9HZf6hYZidB6WodcaGy", - "4FobGn5ZWYquoJkxMzh2VUAWvV36xMgxQ3M7uG1pGGhd", - "76sxKrPtgoJHDJvxwFHqb3cAXWfRHFLe3VpKcLCAHSEf", - "H2cDR3EkJjtTKDQKk8SJS48du9mhsdzQhy8xJx5UMqQK", - "8m5GkL7nVy95G4YVUbs79z873oVKqg2afgKRmqxsiiRm", - "4kuG6NsAFJNwqEkac8GFDMMheCGKUPEbaRVHHyFHSwWz", - "8vFGAKdwpn4hk7kc1cBgfWZzpyW3MEMDATDzVZhddeQb", - "86Vh4XGLW2b6nvWbRyDs4ScgMXbuvRCHT7WbUT3RFxKG", - "DZfEurFKFtSbdWZsKSDTqpqsQgvXxmESpvRtXkAdgLwM", - "5L2QKqDn5ukJSWGyqR4RPvFvwnBabKWqAqMzH4heaQNB", - "DYVeNgXGLAhZdeLMMYnCw1nPnMxkBN7fJnNpHmizTrrF", - "Hbj6XdxX6eV4nfbYTseysibp4zZJtVRRPn2J3BhGRuK9", - "846ah7iBSu9ApuCyEhA5xpnjHHX7d4QJKetWLbwzmJZ8", - "5BqYhuD4q1YD3DMAYkc1FeTu9vqQVYYdfBAmkZjamyZg", - }, - "fomo": { - // FOMO collects fees in USDC. The fee wallet's USDC ATA is monitored for sigs - // (getSignaturesForAddress on the wallet returns 0 overlap with ATA — confirmed - // live on 2026-08). Fee detection uses FeeOwners below (the wallet address) - // because postTokenBalances.owner = wallet, not ATA. - "HrTf9CzXR1dRH4Sof5QrpmGWwpwAf3qZzwCsEjQpXcSq", // USDC ATA of fee wallet - }, - // Trojan — confirmed via DeFiLlama dexs/trojan/index.ts (master, 2026-08) - "trojan": { - "9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco", // primary bot fee wallet - "92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH", // terminal - "2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ", - "65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH", - "BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp", - "8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn", - }, - // Banana Gun — confirmed via DeFiLlama fees/banana-gun-trading.ts (master, 2026-08) - "banana-gun": { - "47hEzz83VFR23rLTEeVm9A7eFzjJwjvdupPPmX3cePqF", - "4BBNEVRgrxVKv9f7pMNE788XM1tt379X9vNjpDH2KCL7", - "8r2hZoDfk5hDWJ1sDujAi2Qr45ZyZw5EQxAXiMZWLKh2", - }, - // Maestro — confirmed via DeFiLlama fees/maestro.ts (master, 2026-08) - "maestro": { - "MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36", - "FRMxAnZgkW58zbYcE7Bxqsg99VWpJh6sMP5xLzAWNabN", - }, -} - -// FeeToken maps platform name → SPL token mint used for platform fee collection. -// Absent (empty string) = SOL (native lamports). Set for USDC-fee platforms. -// USDC mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v -var FeeToken = map[string]string{ - "fomo": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", -} - -// FeeOwners maps platform → wallet addresses to match against postTokenBalances.owner -// for SPL-token fee detection. Only needed when FeeAccounts holds ATA addresses -// (which differ from the wallet). If absent, feeSet falls back to FeeAccounts. -var FeeOwners = map[string][]string{ - "fomo": {"R4rNJHaffSUotNmqSKNEfDcJE8A7zJUkaoM5Jkd7cYX"}, +// FeeAccounts maps platform name → its on-chain fee-receiver account. +// These are the accounts that receive platform fees from user trades. +// Adding a new platform = one line here. +var FeeAccounts = map[string]string{ + "pump.fun": "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", + // TODO: add FOMO, Axiom, GMGN once their fee accounts are confirmed + // Discover by inspecting any known tx from each platform and finding + // the SOL recipient that is not the user, not Jito, not rent. } // JitoTipAccounts is the authoritative set of 8 Jito tip wallets. diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go index 6185da79..1564a23d 100644 --- a/harnesses/solana-exec/internal/store/store.go +++ b/harnesses/solana-exec/internal/store/store.go @@ -69,7 +69,7 @@ func (db *DB) UpsertEvents(ctx context.Context, events []ExecEvent) error { return tx.Commit(ctx) } -// Cursor holds the last processed signature for a fee account. +// Cursor holds the last processed signature for a platform. type Cursor struct { LastSig string Slot uint64 @@ -98,9 +98,8 @@ func (db *DB) SaveCursor(ctx context.Context, platform, lastSig string, slot uin } // UpsertRawCounts stores hourly successful-tx counts from raw sig pagination. -// fromCursor is cursor.LastSig at poll start — makes each row idempotent: -// same (platform, bucket_start, fromCursor) on retry → DO NOTHING, no double-count. -func (db *DB) UpsertRawCounts(ctx context.Context, platform, fromCursor string, counts map[time.Time]int64) error { +// 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 } @@ -111,10 +110,12 @@ func (db *DB) UpsertRawCounts(ctx context.Context, platform, fromCursor string, defer tx.Rollback(ctx) for bucket, count := range counts { _, err := tx.Exec(ctx, ` - 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, + 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) @@ -123,49 +124,6 @@ func (db *DB) UpsertRawCounts(ctx context.Context, platform, fromCursor string, return tx.Commit(ctx) } -// CUSample is a single compute-unit price observation from the enhanced API sample. -type CUSample struct { - Sig string - BlockTime time.Time - CUPriceMicro int64 -} - -// InsertCUSamples stores CU price samples idempotently keyed on (platform, sig). -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, 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 %s: %w", s.Sig, 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 -} - -// PurgeEvents deletes sampled events older than 90 days. -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). @@ -174,7 +132,7 @@ func (db *DB) Materialize(ctx context.Context, platform string) error { INSERT INTO solana_exec_facts (platform, bucket_start, tx_count, avg_priority_fee_lamports, p50_cu_price_micro, p95_cu_price_micro, - avg_platform_fee_lamports, sum_platform_fee_lamports, jito_rate, avg_cu_consumed, computed_at) + avg_platform_fee_lamports, jito_rate, avg_cu_consumed, computed_at) SELECT e.platform, date_trunc('hour', e.block_time) AS bucket_start, @@ -183,22 +141,13 @@ func (db *DB) Materialize(ctx context.Context, platform string) error { 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, - -- Scale sampled fee sum by raw/sampled ratio so the total reflects full volume. - -- Without scaling, sum would be ~1/N of reality for high-volume platforms. - CASE - WHEN r.success_count IS NOT NULL AND COUNT(*) > 0 - THEN (SUM(e.platform_fee_lamports)::numeric * r.success_count / COUNT(*))::bigint - ELSE SUM(e.platform_fee_lamports) - END AS sum_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 e - 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) + 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 @@ -207,7 +156,6 @@ func (db *DB) Materialize(ctx context.Context, platform string) error { p50_cu_price_micro = EXCLUDED.p50_cu_price_micro, p95_cu_price_micro = EXCLUDED.p95_cu_price_micro, avg_platform_fee_lamports = EXCLUDED.avg_platform_fee_lamports, - sum_platform_fee_lamports = EXCLUDED.sum_platform_fee_lamports, jito_rate = EXCLUDED.jito_rate, avg_cu_consumed = EXCLUDED.avg_cu_consumed, computed_at = now()`, diff --git a/harnesses/solana-exec/migrations/003_cu_samples.sql b/harnesses/solana-exec/migrations/003_cu_samples.sql deleted file mode 100644 index 38d89388..00000000 --- a/harnesses/solana-exec/migrations/003_cu_samples.sql +++ /dev/null @@ -1,16 +0,0 @@ -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; diff --git a/harnesses/solana-exec/migrations/003_multi_platform.sql b/harnesses/solana-exec/migrations/003_multi_platform.sql deleted file mode 100644 index bf5528ee..00000000 --- a/harnesses/solana-exec/migrations/003_multi_platform.sql +++ /dev/null @@ -1,29 +0,0 @@ -BEGIN; - --- Add from_cursor to raw_counts for idempotency: same poll window = same from_cursor = DO NOTHING on retry. --- Drop and recreate: table is ephemeral (rolling window, recomputable) so truncate is safe. -TRUNCATE solana_exec_raw_counts; -ALTER TABLE solana_exec_raw_counts - ADD COLUMN IF NOT EXISTS from_cursor TEXT NOT NULL DEFAULT ''; -ALTER TABLE solana_exec_raw_counts - DROP CONSTRAINT IF EXISTS solana_exec_raw_counts_pkey; -ALTER TABLE solana_exec_raw_counts - ADD PRIMARY KEY (platform, bucket_start, from_cursor); - --- Per-account cursors: key was platform, now platform:feeAccount. --- Existing single-account cursors (key = platform name) stay valid for pump.fun. --- No migration needed; old key will not match new compound keys — polls restart from tip for new accounts. - --- CU price samples for rolling percentile metrics (35-day retention). -CREATE TABLE IF NOT EXISTS solana_exec_cu_samples ( - platform TEXT NOT NULL, - sig TEXT NOT NULL, - block_time TIMESTAMPTZ NOT NULL, - cu_price_micro BIGINT NOT NULL, - PRIMARY KEY (platform, sig) -); - -CREATE INDEX IF NOT EXISTS solana_exec_cu_samples_bt - ON solana_exec_cu_samples (platform, block_time DESC); - -COMMIT; diff --git a/harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql b/harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql deleted file mode 100644 index 66ab942a..00000000 --- a/harnesses/solana-exec/migrations/004_raw_counts_idempotent.sql +++ /dev/null @@ -1,17 +0,0 @@ -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; 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 deleted file mode 100644 index 2594ba92..00000000 --- a/harnesses/solana-exec/migrations/005_events_purge_and_cu_sig.sql +++ /dev/null @@ -1,18 +0,0 @@ -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; diff --git a/harnesses/solana-exec/migrations/006_sum_platform_fee.sql b/harnesses/solana-exec/migrations/006_sum_platform_fee.sql deleted file mode 100644 index 65218cc6..00000000 --- a/harnesses/solana-exec/migrations/006_sum_platform_fee.sql +++ /dev/null @@ -1,8 +0,0 @@ -BEGIN; - --- Exact per-bucket fee sum, populated by Materialize(). --- Replaces the extrapolation txCount × avg_platform_fee when events are complete. -ALTER TABLE solana_exec_facts - ADD COLUMN IF NOT EXISTS sum_platform_fee_lamports BIGINT NOT NULL DEFAULT 0; - -COMMIT; diff --git a/harnesses/solana-launchpad-wars/Dockerfile b/harnesses/solana-launchpad-wars/Dockerfile deleted file mode 100644 index 379e2f91..00000000 --- a/harnesses/solana-launchpad-wars/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod ./ -COPY cmd ./cmd -RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o /app/harness ./cmd/script - -FROM debian:bookworm-slim -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/harness /app/harness -EXPOSE 2112 -CMD ["/app/harness"] diff --git a/harnesses/solana-launchpad-wars/cmd/script/main.go b/harnesses/solana-launchpad-wars/cmd/script/main.go deleted file mode 100644 index a7307869..00000000 --- a/harnesses/solana-launchpad-wars/cmd/script/main.go +++ /dev/null @@ -1,71 +0,0 @@ -// solana-launchpad-wars -- Bench 200 -// -// Polls the Mobula lighthouse endpoint every 15 minutes and exposes -// per-launchpad 24h volume, fees and trade counts as Prometheus gauges. -// -// Metrics on :2112/metrics: -// -// solana_launchpad_volume_24h_usd{launchpad} -// solana_launchpad_fees_24h_usd{launchpad} -// solana_launchpad_trades_24h{launchpad} -// solana_launchpad_health{launchpad} -package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - "time" -) - -const pollInterval = 15 * time.Minute - -func main() { - fmt.Println("=== solana-launchpad-wars harness ===") - fmt.Println("OpenChainBench Bench 200 -- Solana launchpad leaderboard via Mobula lighthouse.") - - apiKey := os.Getenv("MOBULA_API_KEY") - if apiKey == "" { - fmt.Fprintln(os.Stderr, "[fatal] MOBULA_API_KEY not set") - os.Exit(1) - } - - client := newMobulaClient(apiKey) - - go func() { - if err := startMetricsServer(":2112"); err != nil { - fmt.Printf("[fatal] metrics server: %v\n", err) - os.Exit(1) - } - }() - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - - runOnce(client) - - tick := time.NewTicker(pollInterval) - defer tick.Stop() - - for { - select { - case <-sig: - fmt.Println("[shutdown] received signal") - return - case <-tick.C: - runOnce(client) - } - } -} - -func runOnce(c *mobulaClient) { - data, err := c.fetchLighthouse() - if err != nil { - fmt.Printf("[poll] lighthouse fetch failed: %v\n", err) - return - } - publishLighthouse(data) - fmt.Printf("[poll] updated: %d launchpads, %d platforms\n", - len(data.ByLaunchpad), len(data.ByPlatform)) -} diff --git a/harnesses/solana-launchpad-wars/cmd/script/metrics.go b/harnesses/solana-launchpad-wars/cmd/script/metrics.go deleted file mode 100644 index 54cb19cb..00000000 --- a/harnesses/solana-launchpad-wars/cmd/script/metrics.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "net/http" - "regexp" - "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - launchpadVol24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_launchpad_volume_24h_usd", - Help: "24h trading volume in USD (Mobula lighthouse byLaunchpad).", - }, []string{"launchpad"}) - - launchpadFees24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_launchpad_fees_24h_usd", - Help: "24h protocol fees in USD (Mobula lighthouse byLaunchpad).", - }, []string{"launchpad"}) - - launchpadTrades24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_launchpad_trades_24h", - Help: "24h trade count (Mobula lighthouse byLaunchpad).", - }, []string{"launchpad"}) - - launchpadHealth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_launchpad_health", - Help: "1 if this launchpad returned data in the last lighthouse poll.", - }, []string{"launchpad"}) - - platformVol24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_platform_volume_24h_usd", - Help: "24h trading volume in USD routed through this Solana trading platform.", - }, []string{"platform"}) - - platformFees24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_platform_fees_24h_usd", - Help: "24h platform fees in USD collected by this Solana trading platform.", - }, []string{"platform"}) - - platformTrades24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_platform_trades_24h", - Help: "24h trade count attributed to this Solana trading platform (Mobula lighthouse).", - }, []string{"platform"}) - - platformHealth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_platform_health", - Help: "1 if this platform returned data in the last lighthouse poll.", - }, []string{"platform"}) -) - -func init() { - prometheus.MustRegister( - launchpadVol24h, - launchpadFees24h, - launchpadTrades24h, - launchpadHealth, - platformVol24h, - platformFees24h, - platformTrades24h, - platformHealth, - ) -} - -var nonAlnum = regexp.MustCompile(`[^a-z0-9]+`) - -func slugify(name string) string { - s := strings.ToLower(name) - s = nonAlnum.ReplaceAllString(s, "-") - return strings.Trim(s, "-") -} - -func publishLighthouse(data *lighthouseData) { - for _, e := range data.ByLaunchpad { - slug := slugify(e.Name) - launchpadVol24h.WithLabelValues(slug).Set(e.VolumeUSD.H24) - launchpadFees24h.WithLabelValues(slug).Set(e.FeesPaidUSD.H24) - launchpadTrades24h.WithLabelValues(slug).Set(e.Trades.H24) - launchpadHealth.WithLabelValues(slug).Set(1) - // pump.fun trades via its own frontend are in byLaunchpad; publish as platform - // so bench 201 can compare it against GMGN, Axiom, Fomo etc. (same attribution logic). - if slug == "pumpfun" { - platformVol24h.WithLabelValues("pump-fun").Set(e.VolumeUSD.H24) - platformFees24h.WithLabelValues("pump-fun").Set(e.FeesPaidUSD.H24) - platformTrades24h.WithLabelValues("pump-fun").Set(e.Trades.H24) - platformHealth.WithLabelValues("pump-fun").Set(1) - } - } - for _, e := range data.ByPlatform { - slug := slugify(e.Name) - platformVol24h.WithLabelValues(slug).Set(e.VolumeUSD.H24) - platformFees24h.WithLabelValues(slug).Set(e.FeesPaidUSD.H24) - platformTrades24h.WithLabelValues(slug).Set(e.Trades.H24) - platformHealth.WithLabelValues(slug).Set(1) - } -} - -func startMetricsServer(addr string) error { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("OK")) - }) - return http.ListenAndServe(addr, mux) -} diff --git a/harnesses/solana-launchpad-wars/cmd/script/mobula.go b/harnesses/solana-launchpad-wars/cmd/script/mobula.go deleted file mode 100644 index a458aa5b..00000000 --- a/harnesses/solana-launchpad-wars/cmd/script/mobula.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -const lighthouseURL = "https://api.mobula.io/api/2/market/lighthouse" - -type mobulaClient struct { - apiKey string - http *http.Client -} - -func newMobulaClient(apiKey string) *mobulaClient { - return &mobulaClient{ - apiKey: apiKey, - http: &http.Client{Timeout: 15 * time.Second}, - } -} - -type windowValues struct { - Min15 float64 `json:"15min"` - H1 float64 `json:"1h"` - H6 float64 `json:"6h"` - H24 float64 `json:"24h"` -} - -type lighthouseEntry struct { - Name string `json:"name"` - VolumeUSD windowValues `json:"volumeUSD"` - FeesPaidUSD windowValues `json:"feesPaidUSD"` - Trades windowValues `json:"trades"` -} - -type lighthouseData struct { - ByLaunchpad []lighthouseEntry `json:"byLaunchpad"` - ByPlatform []lighthouseEntry `json:"byPlatform"` -} - -func (c *mobulaClient) fetchLighthouse() (*lighthouseData, error) { - req, err := http.NewRequest(http.MethodGet, lighthouseURL, nil) - if err != nil { - return nil, fmt.Errorf("build req: %w", err) - } - req.Header.Set("Authorization", c.apiKey) - req.Header.Set("Accept", "application/json") - - resp, err := c.http.Do(req) - if err != nil { - return nil, fmt.Errorf("http: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) - if err != nil { - return nil, fmt.Errorf("read: %w", err) - } - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("status %d: %.200s", resp.StatusCode, body) - } - - var out struct { - Data lighthouseData `json:"data"` - } - if err := json.Unmarshal(body, &out); err != nil { - return nil, fmt.Errorf("parse: %w", err) - } - return &out.Data, nil -} diff --git a/harnesses/solana-launchpad-wars/go.mod b/harnesses/solana-launchpad-wars/go.mod deleted file mode 100644 index 6a1707dd..00000000 --- a/harnesses/solana-launchpad-wars/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module solana-launchpad-wars - -go 1.24.0 - -require github.com/prometheus/client_golang v1.23.2 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.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 - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/sys v0.35.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect -) diff --git a/harnesses/solana-launchpad-wars/go.sum b/harnesses/solana-launchpad-wars/go.sum deleted file mode 100644 index d6b8ca98..00000000 --- a/harnesses/solana-launchpad-wars/go.sum +++ /dev/null @@ -1,46 +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -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/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= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/solana-unique-traders/Dockerfile b/harnesses/solana-unique-traders/Dockerfile deleted file mode 100644 index f88e4a0d..00000000 --- a/harnesses/solana-unique-traders/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod ./ -COPY cmd ./cmd -COPY queries ./queries -RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o /app/harness ./cmd/script - -FROM debian:bookworm-slim -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/harness /app/harness -EXPOSE 2112 -CMD ["/app/harness"] diff --git a/harnesses/solana-unique-traders/cmd/script/dune.go b/harnesses/solana-unique-traders/cmd/script/dune.go deleted file mode 100644 index 92707fd0..00000000 --- a/harnesses/solana-unique-traders/cmd/script/dune.go +++ /dev/null @@ -1,216 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -// querySQL counts unique fee-paying transactions per Solana trading platform. -// Uses the same inflow-only filter as bench 203: only rows where the fee wallet -// actually received SOL (post_balance > pre_balance) or USDC/WSOL tokens -// (post_token_balance > pre_token_balance via token_balance_owner). -// This avoids counting transactions where fee wallet addresses appear as -// program IDs or read-only accounts without any balance change. -const querySQL = ` -WITH fee_wallets AS ( - SELECT address, platform FROM (VALUES - ('CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM','pump-fun'), - ('62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV','pump-fun'), - ('FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz','pump-fun'), - ('7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX','pump-fun'), - ('AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY','pump-fun'), - ('9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz','pump-fun'), - ('G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP','pump-fun'), - ('7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ','pump-fun'), - ('GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS','pump-fun'), - ('BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT','gmgn'), - ('7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA','gmgn'), - ('HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3','gmgn'), - ('ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy','gmgn'), - ('DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9','gmgn'), - ('3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK','gmgn'), - ('DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx','gmgn'), - ('dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ','gmgn'), - ('6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn','gmgn'), - ('7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj','axiom'), - ('4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S','axiom'), - ('CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ','axiom'), - ('AaG6of1gbj1pbDumvbSiTuJhRCRkkUNaWVxijSbWvTJW','axiom'), - ('7oi1L8U9MRu5zDz5syFahsiLUric47LzvJBQX6r827ws','axiom'), - ('9kPrgLggBJ69tx1czYAbp7fezuUmL337BsqQTKETUEhP','axiom'), - ('DKyUs1xXMDy8Z11zNsLnUg3dy9HZf6hYZidB6WodcaGy','axiom'), - ('4FobGn5ZWYquoJkxMzh2VUAWvV36xMgxQ3M7uG1pGGhd','axiom'), - ('76sxKrPtgoJHDJvxwFHqb3cAXWfRHFLe3VpKcLCAHSEf','axiom'), - ('H2cDR3EkJjtTKDQKk8SJS48du9mhsdzQhy8xJx5UMqQK','axiom'), - ('8m5GkL7nVy95G4YVUbs79z873oVKqg2afgKRmqxsiiRm','axiom'), - ('4kuG6NsAFJNwqEkac8GFDMMheCGKUPEbaRVHHyFHSwWz','axiom'), - ('8vFGAKdwpn4hk7kc1cBgfWZzpyW3MEMDATDzVZhddeQb','axiom'), - ('86Vh4XGLW2b6nvWbRyDs4ScgMXbuvRCHT7WbUT3RFxKG','axiom'), - ('DZfEurFKFtSbdWZsKSDTqpqsQgvXxmESpvRtXkAdgLwM','axiom'), - ('5L2QKqDn5ukJSWGyqR4RPvFvwnBabKWqAqMzH4heaQNB','axiom'), - ('DYVeNgXGLAhZdeLMMYnCw1nPnMxkBN7fJnNpHmizTrrF','axiom'), - ('Hbj6XdxX6eV4nfbYTseysibp4zZJtVRRPn2J3BhGRuK9','axiom'), - ('846ah7iBSu9ApuCyEhA5xpnjHHX7d4QJKetWLbwzmJZ8','axiom'), - ('5BqYhuD4q1YD3DMAYkc1FeTu9vqQVYYdfBAmkZjamyZg','axiom'), - ('HrTf9CzXR1dRH4Sof5QrpmGWwpwAf3qZzwCsEjQpXcSq','fomo'), - ('9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco','trojan'), - ('92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH','trojan'), - ('2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ','trojan'), - ('65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH','trojan'), - ('BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp','trojan'), - ('8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn','trojan'), - ('AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH','photon'), - ('MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36','maestro'), - ('FRMxAnZgkW58zbYcE7Bxqsg99VWpJh6sMP5xLzAWNabN','maestro') - ) AS t(address, platform) -), -fee_activity AS ( - SELECT fw.platform, a.tx_id - FROM solana.account_activity a - JOIN fee_wallets fw ON a.address = fw.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address IS NULL - AND a.post_balance > a.pre_balance - UNION - SELECT fw.platform, a.tx_id - FROM solana.account_activity a - JOIN fee_wallets fw ON a.token_balance_owner = fw.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' - AND a.post_token_balance > a.pre_token_balance - UNION - SELECT fw.platform, a.tx_id - FROM solana.account_activity a - JOIN fee_wallets fw ON a.token_balance_owner = fw.address - WHERE a.block_time >= NOW() - INTERVAL '1' DAY - AND a.token_mint_address = 'So11111111111111111111111111111111111111112' - AND a.post_token_balance > a.pre_token_balance -) -SELECT platform, COUNT(DISTINCT tx_id) AS unique_traders_24h -FROM fee_activity -GROUP BY platform -ORDER BY unique_traders_24h DESC -` - -const duneBase = "https://api.dune.com/api/v1" - -type duneClient struct { - apiKey string - http *http.Client -} - -type duneRow struct { - Platform string `json:"platform"` - UniqueTraders24h float64 `json:"unique_traders_24h"` -} - -func newDuneClient(apiKey string) *duneClient { - return &duneClient{ - apiKey: apiKey, - http: &http.Client{Timeout: 30 * time.Second}, - } -} - -func (d *duneClient) req(method, path string, body any) (*http.Response, error) { - var r io.Reader - if body != nil { - b, err := json.Marshal(body) - if err != nil { - return nil, err - } - r = bytes.NewReader(b) - } - req, err := http.NewRequest(method, duneBase+path, r) - if err != nil { - return nil, err - } - req.Header.Set("X-Dune-Api-Key", d.apiKey) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - return d.http.Do(req) -} - -func (d *duneClient) createQuery() (string, error) { - payload := map[string]any{ - "name": "OCB: Solana Unique Traders 24h by Platform", - "query_sql": querySQL, - "is_private": false, - "parameters": []any{}, - } - resp, err := d.req("POST", "/query", payload) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, body) - } - var out struct { - QueryID int `json:"query_id"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", err - } - return fmt.Sprintf("%d", out.QueryID), nil -} - -func (d *duneClient) execute(queryID string) (string, error) { - resp, err := d.req("POST", "/query/"+queryID+"/execute", map[string]any{}) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, body) - } - var out struct { - ExecutionID string `json:"execution_id"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", err - } - return out.ExecutionID, nil -} - -func (d *duneClient) executionState(execID string) (string, error) { - resp, err := d.req("GET", "/execution/"+execID+"/status", nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - var out struct { - State string `json:"state"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", err - } - return out.State, nil -} - -func (d *duneClient) latestResult(queryID string) ([]duneRow, error) { - resp, err := d.req("GET", "/query/"+queryID+"/results", nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode >= 400 { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body) - } - var out struct { - Result struct { - Rows []duneRow `json:"rows"` - } `json:"result"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return nil, err - } - return out.Result.Rows, nil -} diff --git a/harnesses/solana-unique-traders/cmd/script/main.go b/harnesses/solana-unique-traders/cmd/script/main.go deleted file mode 100644 index 793cee93..00000000 --- a/harnesses/solana-unique-traders/cmd/script/main.go +++ /dev/null @@ -1,128 +0,0 @@ -// solana-unique-traders -- Bench 207 -// -// Fetches the Dune query result (unique daily traders per Solana platform) -// and exposes it as Prometheus gauges. Triggers a fresh Dune execution every -// hour; serves the latest cached result every 15 minutes in between. -// -// Required env vars: -// DUNE_API_KEY - Dune Analytics API key -// DUNE_QUERY_ID - ID of the saved Dune query (see queries/unique_traders.sql) -// If not set, the harness creates the query automatically and exits. -// -// Metrics on :2112/metrics: -// solana_platform_unique_traders_24h{platform} -// solana_platform_trader_health{platform} -package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - "time" -) - -const ( - fetchInterval = 15 * time.Minute - refreshInterval = 1 * time.Hour -) - -func main() { - fmt.Println("=== solana-unique-traders harness ===") - fmt.Println("OpenChainBench Bench 207 -- Unique daily traders by Solana platform via Dune.") - - apiKey := os.Getenv("DUNE_API_KEY") - if apiKey == "" { - fmt.Fprintln(os.Stderr, "[fatal] DUNE_API_KEY not set") - os.Exit(1) - } - - queryID := os.Getenv("DUNE_QUERY_ID") - client := newDuneClient(apiKey) - - if queryID == "" { - fmt.Println("[init] DUNE_QUERY_ID not set, creating Dune query...") - id, err := client.createQuery() - if err != nil { - fmt.Fprintf(os.Stderr, "[fatal] failed to create Dune query: %v\n", err) - os.Exit(1) - } - fmt.Printf("[init] Dune query created: %s\n", id) - fmt.Printf("[init] Set DUNE_QUERY_ID=%s and restart the harness.\n", id) - os.Exit(0) - } - - go func() { - if err := startMetricsServer(":2112"); err != nil { - fmt.Printf("[fatal] metrics server: %v\n", err) - os.Exit(1) - } - }() - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - - // On startup: fetch latest cached result immediately, then trigger a fresh execution. - runFetch(client, queryID) - go func() { - execID, err := client.execute(queryID) - if err != nil { - fmt.Printf("[refresh] execute failed: %v\n", err) - return - } - pollUntilDone(client, queryID, execID) - }() - - fetchTick := time.NewTicker(fetchInterval) - refreshTick := time.NewTicker(refreshInterval) - defer fetchTick.Stop() - defer refreshTick.Stop() - - for { - select { - case <-sig: - fmt.Println("[shutdown] received signal") - return - case <-fetchTick.C: - runFetch(client, queryID) - case <-refreshTick.C: - execID, err := client.execute(queryID) - if err != nil { - fmt.Printf("[refresh] execute failed: %v\n", err) - continue - } - go pollUntilDone(client, queryID, execID) - } - } -} - -func runFetch(c *duneClient, queryID string) { - rows, err := c.latestResult(queryID) - if err != nil { - fmt.Printf("[fetch] failed: %v\n", err) - return - } - publishRows(rows) - fmt.Printf("[fetch] updated %d platform(s)\n", len(rows)) -} - -func pollUntilDone(c *duneClient, queryID, execID string) { - for range 30 { - time.Sleep(30 * time.Second) - state, err := c.executionState(execID) - if err != nil { - fmt.Printf("[poll] state check failed: %v\n", err) - return - } - switch state { - case "QUERY_STATE_COMPLETED": - fmt.Printf("[poll] execution %s complete\n", execID) - runFetch(c, queryID) - return - case "QUERY_STATE_FAILED", "QUERY_STATE_CANCELLED", "QUERY_STATE_EXPIRED": - fmt.Printf("[poll] execution %s ended with state %s\n", execID, state) - return - } - } - fmt.Printf("[poll] execution %s timed out waiting\n", execID) -} diff --git a/harnesses/solana-unique-traders/cmd/script/metrics.go b/harnesses/solana-unique-traders/cmd/script/metrics.go deleted file mode 100644 index 86b285a8..00000000 --- a/harnesses/solana-unique-traders/cmd/script/metrics.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - uniqueTraders24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_platform_unique_traders_24h", - Help: "Unique wallet addresses that traded through this Solana platform in the last 24h (Dune Analytics, fee-wallet attribution).", - }, []string{"platform"}) - - traderHealth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_platform_trader_health", - Help: "1 if this platform returned unique-trader data in the last Dune poll.", - }, []string{"platform"}) -) - -func init() { - prometheus.MustRegister(uniqueTraders24h, traderHealth) -} - -func publishRows(rows []duneRow) { - for _, r := range rows { - if r.Platform == "" { - continue - } - uniqueTraders24h.WithLabelValues(r.Platform).Set(r.UniqueTraders24h) - traderHealth.WithLabelValues(r.Platform).Set(1) - } -} - -func startMetricsServer(addr string) error { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("OK")) - }) - return http.ListenAndServe(addr, mux) -} diff --git a/harnesses/solana-unique-traders/go.mod b/harnesses/solana-unique-traders/go.mod deleted file mode 100644 index d5e7f65d..00000000 --- a/harnesses/solana-unique-traders/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module solana-unique-traders - -go 1.24.0 - -require github.com/prometheus/client_golang v1.23.2 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.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 - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/sys v0.35.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect -) diff --git a/harnesses/solana-unique-traders/go.sum b/harnesses/solana-unique-traders/go.sum deleted file mode 100644 index d6b8ca98..00000000 --- a/harnesses/solana-unique-traders/go.sum +++ /dev/null @@ -1,46 +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -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/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= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/solana-unique-traders/queries/unique_traders.sql b/harnesses/solana-unique-traders/queries/unique_traders.sql deleted file mode 100644 index da82fafb..00000000 --- a/harnesses/solana-unique-traders/queries/unique_traders.sql +++ /dev/null @@ -1,143 +0,0 @@ --- Unique daily traders by Solana trading platform --- Source: pump_fun_solana.trades (pump.fun) + solana.account_activity joined with solana.transactions (all others) --- Attribution: fee wallet presence in transaction account keys --- Window: rolling 24 hours --- Used by: OpenChainBench Bench 207 (solana-unique-traders) - -WITH pump_fun AS ( - SELECT - 'pump-fun' AS platform, - COUNT(DISTINCT trader_id) AS unique_traders_24h - FROM pump_fun_solana.trades - WHERE block_time >= NOW() - INTERVAL '1' DAY -), - -fee_activity AS ( - SELECT - CASE - WHEN address IN ( - 'BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT', - '7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA', - 'HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3', - 'ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy', - 'DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9', - '3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK', - 'DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx', - 'dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ', - '6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn' - ) THEN 'gmgn' - WHEN address IN ( - '7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj', - '4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S', - 'CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ', - 'AaG6of1gbj1pbDumvbSiTuJhRCRkkUNaWVxijSbWvTJW', - '7oi1L8U9MRu5zDz5syFahsiLUric47LzvJBQX6r827ws', - '9kPrgLggBJ69tx1czYAbp7fezuUmL337BsqQTKETUEhP', - 'DKyUs1xXMDy8Z11zNsLnUg3dy9HZf6hYZidB6WodcaGy', - '4FobGn5ZWYquoJkxMzh2VUAWvV36xMgxQ3M7uG1pGGhd', - '76sxKrPtgoJHDJvxwFHqb3cAXWfRHFLe3VpKcLCAHSEf', - 'H2cDR3EkJjtTKDQKk8SJS48du9mhsdzQhy8xJx5UMqQK', - '8m5GkL7nVy95G4YVUbs79z873oVKqg2afgKRmqxsiiRm', - '4kuG6NsAFJNwqEkac8GFDMMheCGKUPEbaRVHHyFHSwWz', - '8vFGAKdwpn4hk7kc1cBgfWZzpyW3MEMDATDzVZhddeQb', - '86Vh4XGLW2b6nvWbRyDs4ScgMXbuvRCHT7WbUT3RFxKG', - 'DZfEurFKFtSbdWZsKSDTqpqsQgvXxmESpvRtXkAdgLwM', - '5L2QKqDn5ukJSWGyqR4RPvFvwnBabKWqAqMzH4heaQNB', - 'DYVeNgXGLAhZdeLMMYnCw1nPnMxkBN7fJnNpHmizTrrF', - 'Hbj6XdxX6eV4nfbYTseysibp4zZJtVRRPn2J3BhGRuK9', - '846ah7iBSu9ApuCyEhA5xpnjHHX7d4QJKetWLbwzmJZ8', - '5BqYhuD4q1YD3DMAYkc1FeTu9vqQVYYdfBAmkZjamyZg' - ) THEN 'axiom' - WHEN address IN ( - 'HrTf9CzXR1dRH4Sof5QrpmGWwpwAf3qZzwCsEjQpXcSq' - ) THEN 'fomo' - WHEN address IN ( - '9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco', - '92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH', - '2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ', - '65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH', - 'BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp', - '8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn' - ) THEN 'trojan' - WHEN address IN ( - 'AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH' - ) THEN 'photon' - WHEN address IN ( - 'MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36', - 'FRMxAnZgkW58zbYcE7Bxqsg99VWpJh6sMP5xLzAWNabN' - ) THEN 'maestro' - END AS platform, - tx_id - FROM solana.account_activity - WHERE block_time >= NOW() - INTERVAL '1' DAY - AND address IN ( - -- GMGN (9 wallets) - 'BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT', - '7sHXjs1j7sDJGVSMSPjD1b4v3FD6uRSvRWfhRdfv5BiA', - 'HeZVpHj9jLwTVtMMbzQRf6mLtFPkWNSg11o68qrbUBa3', - 'ByRRgnZenY6W2sddo1VJzX9o4sMU4gPDUkcmgrpGBxRy', - 'DXfkEGoo6WFsdL7x6gLZ7r6Hw2S6HrtrAQVPWYx2A1s9', - '3t9EKmRiAUcQUYzTZpNojzeGP1KBAVEEbDNmy6wECQpK', - 'DymeoWc5WLNiQBaoLuxrxDnDRvLgGZ1QGsEoCAM7Jsrx', - 'dBhdrmwBkRa66XxBuAK4WZeZnsZ6bHeHCCLXa3a8bTJ', - '6TxjC5wJzuuZgTtnTMipwwULEbMPx5JPW3QwWkdTGnrn', - -- AXIOM (20 wallets) - '7LCZckF6XXGQ1hDY6HFXBKWAtiUgL9QY5vj1C4Bn1Qjj', - '4V65jvcDG9DSQioUVqVPiUcUY9v6sb6HKtMnsxSKEz5S', - 'CeA3sPZfWWToFEBmw5n1Y93tnV66Vmp8LacLzsVprgxZ', - 'AaG6of1gbj1pbDumvbSiTuJhRCRkkUNaWVxijSbWvTJW', - '7oi1L8U9MRu5zDz5syFahsiLUric47LzvJBQX6r827ws', - '9kPrgLggBJ69tx1czYAbp7fezuUmL337BsqQTKETUEhP', - 'DKyUs1xXMDy8Z11zNsLnUg3dy9HZf6hYZidB6WodcaGy', - '4FobGn5ZWYquoJkxMzh2VUAWvV36xMgxQ3M7uG1pGGhd', - '76sxKrPtgoJHDJvxwFHqb3cAXWfRHFLe3VpKcLCAHSEf', - 'H2cDR3EkJjtTKDQKk8SJS48du9mhsdzQhy8xJx5UMqQK', - '8m5GkL7nVy95G4YVUbs79z873oVKqg2afgKRmqxsiiRm', - '4kuG6NsAFJNwqEkac8GFDMMheCGKUPEbaRVHHyFHSwWz', - '8vFGAKdwpn4hk7kc1cBgfWZzpyW3MEMDATDzVZhddeQb', - '86Vh4XGLW2b6nvWbRyDs4ScgMXbuvRCHT7WbUT3RFxKG', - 'DZfEurFKFtSbdWZsKSDTqpqsQgvXxmESpvRtXkAdgLwM', - '5L2QKqDn5ukJSWGyqR4RPvFvwnBabKWqAqMzH4heaQNB', - 'DYVeNgXGLAhZdeLMMYnCw1nPnMxkBN7fJnNpHmizTrrF', - 'Hbj6XdxX6eV4nfbYTseysibp4zZJtVRRPn2J3BhGRuK9', - '846ah7iBSu9ApuCyEhA5xpnjHHX7d4QJKetWLbwzmJZ8', - '5BqYhuD4q1YD3DMAYkc1FeTu9vqQVYYdfBAmkZjamyZg', - -- FOMO (1 wallet, USDC ATA) - 'HrTf9CzXR1dRH4Sof5QrpmGWwpwAf3qZzwCsEjQpXcSq', - -- TROJAN (6 wallets) - '9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco', - '92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH', - '2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ', - '65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH', - 'BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp', - '8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn', - -- PHOTON (1 wallet) - 'AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH', - -- MAESTRO (2 wallets) - 'MaestroUL88UBnZr3wfoN7hqmNWFi3ZYCGqZoJJHE36', - 'FRMxAnZgkW58zbYcE7Bxqsg99VWpJh6sMP5xLzAWNabN' - ) -), - -fee_with_signer AS ( - SELECT - fa.platform, - t.fee_payer - FROM fee_activity fa - JOIN solana.transactions t ON t.id = fa.tx_id - WHERE t.success = true - AND fa.platform IS NOT NULL -), - -fee_counts AS ( - SELECT - platform, - COUNT(DISTINCT fee_payer) AS unique_traders_24h - FROM fee_with_signer - GROUP BY platform -) - -SELECT platform, unique_traders_24h FROM pump_fun -UNION ALL -SELECT platform, unique_traders_24h FROM fee_counts -ORDER BY unique_traders_24h DESC diff --git a/harnesses/ws-head-latency/cmd/script/aggregator.go b/harnesses/ws-head-latency/cmd/script/aggregator.go index 89a4a029..87e384bc 100644 --- a/harnesses/ws-head-latency/cmd/script/aggregator.go +++ b/harnesses/ws-head-latency/cmd/script/aggregator.go @@ -147,7 +147,7 @@ func (a *aggregator) settle(height int64) { } } for _, p := range providers { - lagMs := float64(e.arrivals[p].Sub(t0).Nanoseconds()) / 1e6 + lagMs := float64(e.arrivals[p].Sub(t0).Milliseconds()) headLagHist.WithLabelValues(p, a.chain).Observe(lagMs) } headWins.WithLabelValues(winner, a.chain).Inc() diff --git a/next.config.ts b/next.config.ts index 3e70417c..cf1c571b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -262,11 +262,6 @@ const nextConfig: NextConfig = { destination: "/benchmarks/pm-resolution-delay/:path*", permanent: true, }, - { - source: "/apps/exec", - destination: "/apps", - permanent: true, - }, ...chainRedirects, ...rpcClusterRedirects, ]; diff --git a/public/logos/banana-gun.png b/public/logos/banana-gun.png deleted file mode 100644 index 740eca24..00000000 Binary files a/public/logos/banana-gun.png and /dev/null differ diff --git a/public/logos/binance-us.jpg b/public/logos/binance-us.jpg deleted file mode 100644 index 6b24fa2b..00000000 Binary files a/public/logos/binance-us.jpg and /dev/null differ diff --git a/public/logos/binance-us.svg b/public/logos/binance-us.svg deleted file mode 100644 index dfe4c478..00000000 --- a/public/logos/binance-us.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/public/logos/bullx.png b/public/logos/bullx.png deleted file mode 100644 index a822fee4..00000000 Binary files a/public/logos/bullx.png and /dev/null differ diff --git a/public/logos/bybit.jpg b/public/logos/bybit.jpg index 4ea8413e..de2bba1d 100644 Binary files a/public/logos/bybit.jpg and b/public/logos/bybit.jpg differ diff --git a/public/logos/coinbase.jpg b/public/logos/coinbase.jpg deleted file mode 100644 index 287d5fb4..00000000 Binary files a/public/logos/coinbase.jpg and /dev/null differ diff --git a/public/logos/cryptocom.jpg b/public/logos/cryptocom.jpg deleted file mode 100644 index 9212c9ff..00000000 Binary files a/public/logos/cryptocom.jpg and /dev/null differ diff --git a/public/logos/cryptocom.svg b/public/logos/cryptocom.svg deleted file mode 100644 index 0d8d2454..00000000 --- a/public/logos/cryptocom.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/public/logos/dex-screener.png b/public/logos/dex-screener.png deleted file mode 100644 index a86efa35..00000000 Binary files a/public/logos/dex-screener.png and /dev/null differ diff --git a/public/logos/gmgn.jpg b/public/logos/gmgn.jpg deleted file mode 100644 index 079d613c..00000000 Binary files a/public/logos/gmgn.jpg and /dev/null differ diff --git a/public/logos/kraken.jpg b/public/logos/kraken.jpg deleted file mode 100644 index f00da4ff..00000000 Binary files a/public/logos/kraken.jpg and /dev/null differ diff --git a/public/logos/maestro.jpg b/public/logos/maestro.jpg deleted file mode 100644 index 070bff5e..00000000 Binary files a/public/logos/maestro.jpg and /dev/null differ diff --git a/public/logos/moonshot.svg b/public/logos/moonshot.svg deleted file mode 100644 index 6e67b8ab..00000000 --- a/public/logos/moonshot.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/logos/phantom.svg b/public/logos/phantom.svg deleted file mode 100644 index 9cc90aa9..00000000 --- a/public/logos/phantom.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/public/logos/photon.webp b/public/logos/photon.webp deleted file mode 100644 index 38238ce0..00000000 Binary files a/public/logos/photon.webp and /dev/null differ diff --git a/public/logos/pump-fun.jpg b/public/logos/pump-fun.jpg deleted file mode 100644 index 710d65cc..00000000 Binary files a/public/logos/pump-fun.jpg and /dev/null differ diff --git a/public/logos/pumpfun.svg b/public/logos/pumpfun.svg deleted file mode 100644 index c264f37f..00000000 --- a/public/logos/pumpfun.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/public/logos/trojan.jpg b/public/logos/trojan.jpg deleted file mode 100644 index 72f4d9bd..00000000 Binary files a/public/logos/trojan.jpg and /dev/null differ diff --git a/src/app/alternatives/[slug]/page.tsx b/src/app/alternatives/[slug]/page.tsx index 05dfd25f..a7b35462 100644 --- a/src/app/alternatives/[slug]/page.tsx +++ b/src/app/alternatives/[slug]/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { ArrowLeft, ArrowUpRight } from "lucide-react"; import { Pill } from "@/components/pill"; import { TimeSeriesChart } from "@/components/time-series-chart"; +import { LedgerTable } from "@/components/ledger-table"; import { CountLeaderboard } from "@/components/count-leaderboard"; import { fmtUnit, unitSuffix, fmtValue } from "@/lib/format"; import { computeFieldStats } from "@/lib/stats"; @@ -22,7 +23,6 @@ import { isRegion } from "@/lib/brand"; import { PERP_VENUE_META, benchRowsForVenue } from "@/lib/perp-venue-context"; import { fetchPerpCohort } from "@/lib/perp-stats"; import { PerpVenueBenchCards } from "@/components/perp-venue-bench-cards"; -import { LedgerTable } from "@/components/ledger-table"; export const dynamic = "force-dynamic"; @@ -50,36 +50,13 @@ export async function generateMetadata({ const title = alt.seo_title ?? `${alt.target_product} alternatives. live benchmark · OpenChainBench`; - const description = capDescription( - alt.seo_description ?? alt.intro, - 158, - ); + const description = alt.seo_description ?? alt.intro.slice(0, 200); const url = `${SITE.url}/alternatives/${alt.slug}`; - const ogImage = `${SITE.url}/api/og/${alt.benchmark}`; - // Noindex when the underlying bench has no live data yet — same rule as - // /benchmarks/[slug]. Avoids indexing a "Not measured yet" page that - // undercuts the bench canonical once data arrives. - const isDraft = alt.bench.status === "draft" || alt.status === "draft"; return { title, description, alternates: { canonical: url }, - ...(isDraft ? { robots: { index: false, follow: true } } : {}), - openGraph: { - title, - description, - type: "article", - url, - siteName: SITE.name, - images: [ogImage], - }, - twitter: { - card: "summary_large_image", - site: SITE.twitter, - title, - description, - images: [ogImage], - }, + openGraph: { title, description, type: "article", url }, }; } @@ -291,8 +268,8 @@ export default async function AlternativePage({ )} {/* Top alternatives cards - explicit list with internal links to - product pages. Excludes the target product itself so every card - is a genuine alternative, not a restatement of the subject. */} + product pages. The bench leaderboard below carries the full data; + this section frames the answer for skim-readers and search. */} {topAlternatives.length > 0 && (
@@ -335,24 +312,20 @@ export default async function AlternativePage({
)} - {/* Count benches: leaderboard is the primary view; no latency stats. */} + {/* Reuse bench rendering. count vs latency split, same as the + /benchmarks/[slug] page. */} {!isDraft && bench.unit === "count" && ( -
+ <> -
+
+ Product ledger + +
+ )} - {/* Latency / rate / USD benches: full ranked table + trend chart. - LedgerTable gives readers the complete ranked list on this page - (better search-intent match than a CTA-only pattern) while - /benchmarks/ remains canonical for the interactive bar chart, - distribution view, and per-chain drilldowns. */} {!isDraft && bench.unit !== "count" && ( <> -
- -
-
{bench.metric} · last 24 hours - - )} - {/* Link to canonical benchmark for interactive views not present here: - ranked bar chart, distribution, per-chain drilldowns, chart export. */} - {!isDraft && ( -
-

- Interactive bar chart, distribution view, per-chain drilldowns and - chart export on the benchmark page. -

- - Open benchmark - - -
+
+ Product ledger · sorted by p50 + +
+ )} -

- Measurements sourced from{" "} - - {bench.title} +

+ Same data as{" "} + + /benchmarks/{bench.slug} - {alt.chain ? ` (filtered to ${alt.chain})` : ""}. Methodology and - raw data on the benchmark page. + {alt.chain ? ` (filtered to ${alt.chain})` : ""}, refreshed every minute. + OpenChainBench is community-run; methodology is open.

); } + diff --git a/src/app/api/bench/hyperliquid-frontends/history/route.test.ts b/src/app/api/bench/hyperliquid-frontends/history/route.test.ts index fc95439f..f17eb47b 100644 --- a/src/app/api/bench/hyperliquid-frontends/history/route.test.ts +++ b/src/app/api/bench/hyperliquid-frontends/history/route.test.ts @@ -211,7 +211,7 @@ describe("GET /api/bench/hyperliquid-frontends/history", () => { source: "archive", rows: [ { - slug: "phantom", + slug: "0xphantom", name: "Phantom", volume_usd: 9_000_000, fees_usd: 90_000, @@ -219,7 +219,7 @@ describe("GET /api/bench/hyperliquid-frontends/history", () => { rank: 1, }, { - slug: "metamask", + slug: "0xmetamask", volume_usd: 4_500_000, rank: 2, }, diff --git a/src/app/api/internal/codex-token/route.ts b/src/app/api/internal/codex-token/route.ts deleted file mode 100644 index c921b979..00000000 --- a/src/app/api/internal/codex-token/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -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' }, - }); -} diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx index c7b580ca..70ceda2b 100644 --- a/src/app/apps/page.tsx +++ b/src/app/apps/page.tsx @@ -2,130 +2,22 @@ import type { Metadata } from "next"; import { pageMetadata } from "@/lib/page-metadata"; import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; import { SITE } from "@/data/site"; -import { fetchEVMRevenue } from "@/lib/evm-exec"; -import { fetchExecLeaderboard } from "@/lib/solana-exec"; -import { fetchSolPrice } from "@/lib/sol-price"; -import { fetchFOMORelayFees } from "@/lib/dune"; -import { TRADING_APPS } from "@/lib/trading-apps-config"; -import { fetchDeFiLlamaData } from "@/lib/defillama"; -import { TradingAppsLeaderboard, type UnifiedAppRow } from "@/components/trading-apps-leaderboard"; -import { RevenueSummary } from "@/components/revenue-summary"; -import Link from "next/link"; +import { fetchAppsLeaderboard, type ProtocolRow } from "@/lib/apps-leaderboard"; +import { AppsLeaderboardTable } from "@/components/apps-leaderboard-table"; const DESCRIPTION = - "Protocol fees collected by trading apps and meme trading terminals — Solana, Ethereum, BSC. On-chain data, updated every 5 min."; + "Protocol revenue leaderboard for on-chain apps: fees captured by treasury, token holders, and LPs. Reproducible methodology, refreshed daily, sources public."; export const metadata: Metadata = pageMetadata({ path: "/apps", - title: "Trading App Revenue — pump.fun, Axiom, GMGN, BullX | OpenChainBench", + title: "Protocol Revenue Leaderboard 2026 — OpenChainBench", description: DESCRIPTION, }); export const revalidate = 300; -const WINDOWS = ["24h", "7d", "30d"] as const; - export default async function AppsHubPage() { - const dlSlugMap = Object.fromEntries( - TRADING_APPS.filter((a) => a.defillamaSlug).map((a) => [a.id, a.defillamaSlug!]) - ); - - const [evmData, solanaData, solPrice, fomoRelay, dlData] = await Promise.all([ - fetchEVMRevenue(), - fetchExecLeaderboard(), - fetchSolPrice(), - fetchFOMORelayFees(), - fetchDeFiLlamaData(dlSlugMap), - ]); - - // Market share = each platform's DL 24h fees / cohort total (active only) - const activeDlTotal = TRADING_APPS.filter((a) => !a.inactive && a.defillamaSlug) - .reduce((sum, a) => sum + (dlData.get(a.id)?.total24h ?? 0), 0); - - const evmByPlatform = new Map( - (evmData?.platforms ?? []).map((p) => [p.platform, p]) - ); - - const solanaByPlatform = new Map( - (solanaData?.platforms ?? []).map((p) => [p.platform, p]) - ); - - const rows: UnifiedAppRow[] = TRADING_APPS.map((meta) => { - const evmRow = meta.evmKey ? evmByPlatform.get(meta.evmKey) : undefined; - const solRow = meta.solanaKey ? solanaByPlatform.get(meta.solanaKey) : undefined; - - const rhRow = meta.robinhoodKey ? evmByPlatform.get(meta.robinhoodKey) : undefined; - - const ethChain = evmRow?.chains["ethereum"]; - const bscChain = evmRow?.chains["bsc"]; - const baseChain = evmRow?.chains["base"]; - const rhChain = rhRow?.chains["robinhood"]; - - const windows: UnifiedAppRow["windows"] = {}; - - for (const w of WINDOWS) { - let solanaFees: number | null = null; - if (solRow) { - const wData = solRow.windows[w]; - if (wData) { - if (meta.solanaFeeIsUSDC) { - // Fees collected in USDC (6 dec): raw units / 1e6 = USD directly - solanaFees = wData.sumPlatformFeeLamports / 1e6; - } else if (solPrice !== null) { - solanaFees = wData.sumPlatformFeeLamports / 1e9 * solPrice; - } - } - } - - // FOMO: supplement on-chain fees with off-chain relay fees from Dune - // (relay accounts for ~96% of FOMO's actual revenue) - if (meta.id === "fomo" && fomoRelay) { - const relayFee = w === "24h" ? fomoRelay.fees24h : w === "7d" ? fomoRelay.fees7d : fomoRelay.fees30d; - solanaFees = (solanaFees ?? 0) + relayFee; - } - - // EVM: 24h adds native ETH/BNB in USD; 7d/30d stable only (no historical prices). - const evmStable = (chain: typeof ethChain) => - !chain ? null : w === "24h" ? chain.stable24h : w === "7d" ? chain.stable7d : chain.stable30d; - const evmNative = (chain: typeof ethChain) => - w === "24h" && chain ? (chain.native?.usd ?? 0) : 0; - - const ethFees = ethChain ? evmStable(ethChain)! + evmNative(ethChain) : null; - const bscFees = bscChain ? evmStable(bscChain)! + evmNative(bscChain) : null; - const baseFees = baseChain ? evmStable(baseChain)! + evmNative(baseChain) : null; - const rhFees = rhChain ? evmStable(rhChain)! + evmNative(rhChain) : null; - const evmTotal = (ethFees ?? 0) + (bscFees ?? 0) + (baseFees ?? 0) + (rhFees ?? 0); - - windows[w] = { - solana: solanaFees, - ethereum: ethFees, - bsc: bscFees, - base: baseFees, - robinhood: rhFees, - total: (solanaFees ?? 0) + evmTotal, - }; - } - - const dl = dlData.get(meta.id) ?? null; - const marketSharePct = dl && activeDlTotal > 0 && !meta.inactive - ? (dl.total24h / activeDlTotal) * 100 - : null; - - return { - meta, - windows, - stableOnly: { - ethereum: ethChain?.coverage === "stable-only", - bsc: bscChain?.coverage === "stable-only", - base: baseChain?.coverage === "stable-only", - robinhood: rhChain?.coverage === "stable-only", - }, - dl, - marketSharePct, - }; - }); - - const updatedAt = evmData?.updatedAt ?? solanaData?.updatedAt ?? null; + const data = await fetchAppsLeaderboard(); const breadcrumb = { "@context": "https://schema.org", @@ -144,58 +36,22 @@ export default async function AppsHubPage() { />

- Trading app revenue. + Protocol revenue.

{DESCRIPTION}

- +
-

- Solana fees = sum(platform_fee) / 1e9 × SOL price. - EVM fees = stable (USDC/USDT) + native where traceable, always 24h. +

+ Gross fees = all fees collected by the protocol before any distribution.{" "} + Net value captured = burn + treasury + token holder − emissions. + Builder/interface fees (paid to frontend operators) are excluded. + Methodology versions and allocation params are on-chain verifiable.

- - {evmData && evmData.platforms.length > 0 && ( -
- -
- )} - -
-

Related benchmarks

-
- - -
-

App Store Ratings

-

iOS ratings for crypto trading apps, live

-
- - - - -
-

Execution Quality

-

Priority fees, Jito rates, platform fees

-
- - -
-
); } diff --git a/src/app/benchmarks/[slug]/[chain]/page.tsx b/src/app/benchmarks/[slug]/[chain]/page.tsx index cd9d9a35..7564acef 100644 --- a/src/app/benchmarks/[slug]/[chain]/page.tsx +++ b/src/app/benchmarks/[slug]/[chain]/page.tsx @@ -5,7 +5,7 @@ import { ArrowLeft, ArrowUpRight } from "lucide-react"; import { getBenchmark } from "@/data/benchmarks"; import { Breadcrumb } from "@/components/breadcrumb"; import { citableAsOf } from "@/lib/citation"; -import { liveResults, MIN_DISPLAY_SUCCESS_PCT } from "@/lib/provider-filters"; +import { liveResults } from "@/lib/provider-filters"; import { fmtUnit } from "@/lib/format"; import { capDescription } from "@/lib/seo-text"; import { getBenchCreatedAt } from "@/lib/seo/bench-dates"; @@ -115,11 +115,9 @@ function resolveLeftoverPlaceholders(text: string, b: Benchmark): string { } function sortLive(results: ProviderResult[], higherIsBetter: boolean) { - return [...liveResults(results)] - .filter((r) => (r.successRate ?? 100) >= MIN_DISPLAY_SUCCESS_PCT) - .sort((a, b) => - higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50, - ); + return [...liveResults(results)].sort((a, b) => + higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50, + ); } async function loadChainPage( @@ -256,9 +254,7 @@ function buildKeyFacts(data: ChainPageData): string { if (data.shape === "row") { const { result, sorted, rank } = data; const subject = `${result.name} ${metricPhrase(b.metric)}`; - const hasGoodData = result.ms.p50 > 0 && - (result.successRate == null || result.successRate >= MIN_DISPLAY_SUCCESS_PCT); - if (!hasGoodData) { + if (result.ms.p50 <= 0) { return `${subject} is measured continuously on this benchmark. Live numbers will appear here as soon as the harness reports fresh samples.`; } const p50 = fmtUnit(result.ms.p50, b.unit); @@ -307,43 +303,23 @@ export async function generateMetadata({ ); const canonical = `${SITE.url}/benchmarks/${data.benchmark.slug}/${chain}`; const ogImage = `${SITE.url}/api/og/${data.benchmark.slug}`; - const b = data.benchmark; - const isDraft = b.status === "draft"; - const isAwaiting = isDraft && b.editorialStatus === "live"; - const isoPubDate = b.lastRunAt - ? new Date(b.lastRunAt).toISOString().slice(0, 10) - : undefined; return { title, description, alternates: { canonical }, - ...(isAwaiting ? { robots: { index: false, follow: true } } : {}), openGraph: { title, description, type: "article", url: canonical, - siteName: SITE.name, images: [ogImage], }, twitter: { card: "summary_large_image", - site: SITE.twitter, title, description, images: [ogImage], }, - other: { - citation_title: title, - citation_author: "OpenChainBench", - citation_publisher: "OpenChainBench", - ...(isoPubDate ? { citation_publication_date: isoPubDate } : {}), - citation_doi: "10.5281/zenodo.20800312", - citation_pdf_url: `${SITE.url}/api/stat/${b.slug}`, - citation_public_url: canonical, - citation_language: "en", - citation_journal_title: "OpenChainBench", - }, }; } diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index d5e6f21a..96d1188f 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -109,11 +109,6 @@ export async function generateMetadata({ // `isAwaiting = isDraft && editorialStatus === "live"` gate below. const metaIsDraft = b.status === "draft"; const metaIsAwaiting = metaIsDraft && b.editorialStatus === "live"; - // RPC benches with fewer than 3 providers are thin-content: a single - // provider leaderboard has no comparative value and no search demand. - // noindex/follow keeps crawl equity flowing without letting an - // empty-looking table rank for "fastest X rpc". - const metaThinRpc = b.category === "RPCs" && (b.results?.length ?? 0) < 3; const metaTitle = b.seoTitle ?? b.title; // Description precedence (most-to-least specific): // 1. `seo_description` from the YAML - hand-crafted snippet with the @@ -151,7 +146,7 @@ export async function generateMetadata({ title: metaTitle, description, alternates: { canonical }, - ...((metaIsAwaiting || metaThinRpc) + ...(metaIsAwaiting ? { robots: { index: false, follow: true } } : {}), openGraph: { diff --git a/src/app/benchmarks/trading-app-execution/page.tsx b/src/app/benchmarks/trading-app-execution/page.tsx deleted file mode 100644 index babfe560..00000000 --- a/src/app/benchmarks/trading-app-execution/page.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import type { Metadata } from "next"; -import { readFileSync } from "fs"; -import { join } from "path"; -import yaml from "js-yaml"; -import Link from "next/link"; -import { pageMetadata } from "@/lib/page-metadata"; -import { safeJsonLd, buildBreadcrumbJsonLd, buildFaqPageJsonLd } from "@/lib/jsonld"; -import { SITE } from "@/data/site"; -import { fetchExecLeaderboard } from "@/lib/solana-exec"; -import { ExecBenchTable } from "@/components/exec-bench-table"; - -export const revalidate = 60; - -type BenchMeta = { - faq: { q: string; a: string }[]; - methodology: string[]; - findings: string[]; - subtitle: string; -}; - -function loadBenchMeta(): BenchMeta { - const raw = readFileSync( - join(process.cwd(), "benchmarks/trading-app-execution.yml"), - "utf-8", - ); - return yaml.load(raw) as BenchMeta; -} - -export const metadata: Metadata = pageMetadata({ - path: "/benchmarks/trading-app-execution", - title: - "Trading App Execution Quality: Axiom vs GMGN vs Trojan vs Maestro | OpenChainBench", - description: - "Compare on-chain execution quality for Solana trading apps: Jito rate, CU price, priority fee, platform fee. Axiom vs GMGN vs Trojan vs Maestro, measured passively from fee accounts.", -}); - -export default async function TradingAppExecutionPage() { - const [data, meta] = await Promise.all([ - fetchExecLeaderboard(), - Promise.resolve(loadBenchMeta()), - ]); - - const breadcrumb = buildBreadcrumbJsonLd([ - { name: "Home", item: SITE.url }, - { name: "Benchmarks", item: `${SITE.url}/benchmarks` }, - { - name: "Trading App Execution", - item: `${SITE.url}/benchmarks/trading-app-execution`, - }, - ]); - - const faqJsonLd = buildFaqPageJsonLd( - meta.faq, - `${SITE.url}/benchmarks/trading-app-execution`, - ); - - return ( -
-