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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions benchmarks/app-store-ratings.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,8 @@
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, 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 Fomo vs PumpFun vs GMGN vs Moonshot ranked live from iTunes ratings."
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
Expand DownExpand Up@@ -51,6 +51,7 @@ methodology:
- "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, copy trading, and 25k+ referrals via its registered builder code."

findings:
- "{{best_name}} leads crypto trading apps on the iOS App Store at {{best_p50}} stars."
Expand DownExpand Up@@ -226,3 +227,16 @@ providers:
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"}
1 change: 1 addition & 0 deletions harnesses/app-store-ratings/cmd/script/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ var apps = []struct {
{"binance-us", "1492670702"},
{"robinhood", "938003185"},
{"cryptocom", "1262148500"},
{"invo", "1601301148"},
}

func main() {
Expand Down
14 changes: 9 additions & 5 deletions harnesses/evm-exec/cmd/collector/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,13 +224,17 @@ func collectNativeBlockscout(ctx context.Context, db *store.DB, plt, chain, coll
}
rpc := chainRPC[chain]

txs, lastBlock, err := source.GetBlockscoutInternalTxs(ctx, source.BlockscoutRobinhood, collector, cursor)
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 txs {
for _, tx := range append(internalTxs, normalTxs...) {
if tx.BlockNum > toBlock {
continue
}
Expand All@@ -253,11 +257,11 @@ func collectNativeBlockscout(ctx context.Context, db *store.DB, plt, chain, coll
if err := db.UpsertEvents(ctx, events); err != nil {
return err
}
if lastBlock > cursor {
_ = db.SaveCursor(ctx, chain, plt, "native", lastBlock)
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), lastBlock)
log.Printf("collector: %s/%s native ETH (Blockscout) %d events block=%d", plt, chain, len(events), max64(lastInt, lastNorm))
}
return nil
}
Expand Down
134 changes: 134 additions & 0 deletions harnesses/evm-exec/internal/source/blockscout.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,17 @@ type blockscoutTx struct {
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) {
Expand DownExpand Up@@ -153,3 +164,126 @@ func GetBlockscoutInternalTxs(ctx context.Context, apiURL, address string, start

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
}
4 changes: 2 additions & 2 deletions harnesses/evm-exec/internal/store/store.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,8 +146,8 @@ func (db *DB) Materialize(ctx context.Context) error {
chain,
platform,
date_trunc('hour', block_time) AS bucket_start,
COALESCE(SUM(amount_raw / POW(10, decimals)) FILTER (WHERE asset <> 'native'), 0),
COALESCE(SUM(amount_raw / POW(10, decimals)) FILTER (WHERE asset = 'native'), 0),
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
Expand Down
13 changes: 10 additions & 3 deletions harnesses/solana-exec/cmd/api/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -234,10 +234,17 @@ func mustEnv(key string) string {
// ---- 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 tracked.
// "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{
"gmgn": {"ethereum": "full", "bsc": "full", "base": "stable-only"},
"pumpfun": {"ethereum": "full", "bsc": "full", "base": "stable-only"},
"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.
Expand Down
1 change: 1 addition & 0 deletions src/lib/brand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,6 +78,7 @@ const BRANDS: Record<string, Brand> = {
across: { color: "#6CF9D8" }, // across aqua

// ─── Crypto trading apps (bench № 202) ───
invo: { color: "#7B5EA7" }, // invo purple
robinhood: { color: "#00C805" }, // robinhood green
cryptocom: { color: "#002D74", dark: true }, // crypto.com navy
pumpfun: { color: "#00C851" }, // pump.fun green
Expand Down
4 changes: 2 additions & 2 deletions src/lib/trading-apps-config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,8 @@ export const TRADING_APPS: AppMeta[] = [
{ id: "bullx", name: "BullX", category: "trading-terminal", logoKey: "bullx", productUrl: "https://bullx.io", benchUrl: "/benchmarks/trading-app-execution", evmKey: null, solanaKey: "bullx", robinhoodKey: null, inactive: true, inactiveSince: "2026-06-01" },
{ id: "photon", name: "Photon", category: "trading-terminal", logoKey: "photon", productUrl: "https://photon-sol.tinyastro.io", benchUrl: "/benchmarks/trading-app-execution", evmKey: null, solanaKey: "photon", robinhoodKey: null },
{ id: "gmgn", name: "GMGN", category: "telegram-bot", logoKey: "gmgn", productUrl: "https://gmgn.ai", benchUrl: "/benchmarks/trading-app-execution", evmKey: "gmgn", solanaKey: "gmgn", robinhoodKey: "gmgn-robinhood" },
{ id: "axiom", name: "Axiom", category: "telegram-bot", logoKey: "axiom", productUrl: "https://axiom.trade", benchUrl: "/benchmarks/trading-app-execution", evmKey: "axiom", solanaKey: "axiom", robinhoodKey: null, inactive: true, inactiveSince: "2026-06-29" },
{ id: "axiom", name: "Axiom", category: "telegram-bot", logoKey: "axiom", productUrl: "https://axiom.trade", benchUrl: "/benchmarks/trading-app-execution", evmKey: "axiom", solanaKey: "axiom", robinhoodKey: null },
{ id: "maestro", name: "Maestro", category: "telegram-bot", logoKey: "maestro", productUrl: "https://maestro.bots.gg", benchUrl: "/benchmarks/trading-app-execution", evmKey: "maestro", solanaKey: "maestro", robinhoodKey: "maestro-robinhood" },
{ id: "banana-gun", name: "Banana Gun", category: "telegram-bot", logoKey: "banana-gun", productUrl: "https://t.me/BananaGunSniper_bot", benchUrl: "/benchmarks/trading-app-execution", evmKey: "banana-gun", solanaKey: "banana-gun", robinhoodKey: null },
{ id: "banana-gun", name: "Banana Gun", category: "telegram-bot", logoKey: "banana-gun", productUrl: "https://t.me/BananaGunSniper_bot", benchUrl: "/benchmarks/trading-app-execution", evmKey: null, solanaKey: "banana-gun", robinhoodKey: null },
{ id: "trojan", name: "Trojan", category: "telegram-bot", logoKey: "trojan", productUrl: "https://trojan.bot", benchUrl: "/benchmarks/trading-app-execution", evmKey: null, solanaKey: "trojan", robinhoodKey: null },
];
Loading