diff --git a/benchmarks/app-store-ratings.yml b/benchmarks/app-store-ratings.yml index 25756781..7bc4360c 100644 --- a/benchmarks/app-store-ratings.yml +++ b/benchmarks/app-store-ratings.yml @@ -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 @@ -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." @@ -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"} diff --git a/harnesses/app-store-ratings/cmd/script/main.go b/harnesses/app-store-ratings/cmd/script/main.go index 1c9b7b41..67ad2295 100644 --- a/harnesses/app-store-ratings/cmd/script/main.go +++ b/harnesses/app-store-ratings/cmd/script/main.go @@ -23,6 +23,7 @@ var apps = []struct { {"binance-us", "1492670702"}, {"robinhood", "938003185"}, {"cryptocom", "1262148500"}, + {"invo", "1601301148"}, } func main() { diff --git a/harnesses/evm-exec/cmd/collector/main.go b/harnesses/evm-exec/cmd/collector/main.go index 0919ddcf..d72a5c42 100644 --- a/harnesses/evm-exec/cmd/collector/main.go +++ b/harnesses/evm-exec/cmd/collector/main.go @@ -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 } @@ -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 } diff --git a/harnesses/evm-exec/internal/source/blockscout.go b/harnesses/evm-exec/internal/source/blockscout.go index f6519c64..06ca775f 100644 --- a/harnesses/evm-exec/internal/source/blockscout.go +++ b/harnesses/evm-exec/internal/source/blockscout.go @@ -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) { @@ -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 +} diff --git a/harnesses/evm-exec/internal/store/store.go b/harnesses/evm-exec/internal/store/store.go index 0db698c2..35ad6595 100644 --- a/harnesses/evm-exec/internal/store/store.go +++ b/harnesses/evm-exec/internal/store/store.go @@ -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 diff --git a/harnesses/solana-exec/cmd/api/main.go b/harnesses/solana-exec/cmd/api/main.go index e20f8bd4..24181a6a 100644 --- a/harnesses/solana-exec/cmd/api/main.go +++ b/harnesses/solana-exec/cmd/api/main.go @@ -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. diff --git a/src/lib/brand.ts b/src/lib/brand.ts index d829e075..84a60b40 100644 --- a/src/lib/brand.ts +++ b/src/lib/brand.ts @@ -78,6 +78,7 @@ const BRANDS: Record = { 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 diff --git a/src/lib/trading-apps-config.ts b/src/lib/trading-apps-config.ts index 6c75d051..fbfd71cf 100644 --- a/src/lib/trading-apps-config.ts +++ b/src/lib/trading-apps-config.ts @@ -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 }, ];