From 2318510530111942184642faf5b0fe7f15c6d48d Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:34:12 +0200 Subject: [PATCH 1/8] bench 265 perp-pf-ratio: perp DEX price-to-fees valuation table (dev-only) + widen bench 234 (#2340) Harness perp-token-metrics now publishes the full valuation row per protocol: P/F (mcap / annualized fees), FDV/F, P/S, fees 24h/30d/prev 30d/1y, revenue 30d + share, market cap, FDV, float %, open interest and fees/OI, for 13 tokened perp DEXes plus 5 token-less venues (fees + OI only). Windows are cut from DeFiLlama totalDataChart identically for every protocol (30d sum x 365/30); tokens are measured against the DeFiLlama parent they accrue from so mcap and fees share one scope. Ratios with a zero denominator or no token are deleted from the gauge vector, never published as 0. Unit tests cover window cutting, ratio math and the fetchers against a fake upstream; one live poll verified 18/18 protocols healthy. Bench 265 spec renders a 6-column ledger (P/F, FDV/fees, P/S, Fees 30d, Market cap, Float) plus panel tabs for revenue, OI and fees/OI. Held in REMOVED_BENCH_SLUGS until the Railway harness is redeployed; bench-set cache keys bumped (v65 / v60). Bench 234 gains the five new protocols that publish revenue and switches dYdX to the dYdX Chain token. computeFieldStats now prunes all-zero rows the way the ledger already does, so a token-less venue promoted to "live" by its panel data no longer drags the Best stat to 0. Claude-Session: https://claude.ai/code/session_01L4rjxemTvgRwrCG6D2iK5D Co-authored-by: Flotapponnier Co-authored-by: Claude Opus 5 (1M context) (cherry picked from commit 3635b0a94909d17cd1ee111d66d43d5453296580) --- benchmarks/perp-pe-ratio.yml | 76 +++- benchmarks/perp-pf-ratio.yml | 396 ++++++++++++++++++ harnesses/perp-token-metrics/README.md | 79 ++++ .../perp-token-metrics/cmd/script/fetcher.go | 170 ++++++-- .../perp-token-metrics/cmd/script/main.go | 132 ++++-- .../perp-token-metrics/cmd/script/metrics.go | 81 ++-- .../cmd/script/valuation.go | 77 ++++ .../cmd/script/valuation_test.go | 226 ++++++++++ public/logos/apex.webp | Bin 0 -> 29302 bytes public/logos/avantis.webp | Bin 0 -> 1046 bytes public/logos/derive.webp | Bin 0 -> 37074 bytes src/data/bench-published.json | 1 + src/data/provider-registry.ts | 19 + src/lib/logo-manifest.ts | 4 + src/lib/removed-benches.ts | 6 + src/lib/spec.ts | 6 +- src/lib/stats.test.ts | 41 ++ src/lib/stats.ts | 12 +- 18 files changed, 1224 insertions(+), 102 deletions(-) create mode 100644 benchmarks/perp-pf-ratio.yml create mode 100644 harnesses/perp-token-metrics/README.md create mode 100644 harnesses/perp-token-metrics/cmd/script/valuation.go create mode 100644 harnesses/perp-token-metrics/cmd/script/valuation_test.go create mode 100644 public/logos/apex.webp create mode 100644 public/logos/avantis.webp create mode 100644 public/logos/derive.webp create mode 100644 src/lib/stats.test.ts diff --git a/benchmarks/perp-pe-ratio.yml b/benchmarks/perp-pe-ratio.yml index 770ca15d9..90fd9ffdf 100644 --- a/benchmarks/perp-pe-ratio.yml +++ b/benchmarks/perp-pe-ratio.yml @@ -2,9 +2,9 @@ slug: perp-pe-ratio number: "234" -title: "Perp DEX token P/E ratio 2026: Hyperliquid, GMX, dYdX, GNS live" +title: "Perp DEX token P/E ratio 2026: Hyperliquid, Lighter, GMX, dYdX, GNS live" seo_title: "Perp DEX token P/E ratio 2026: Hyperliquid vs GMX vs dYdX vs GNS" -seo_description: "{{best_name}} has the lowest P/E ratio at {{best_p50}}x among perp DEX tokens. FDV divided by annualized protocol revenue for Hyperliquid, GMX, Gains, dYdX, Drift, Ostium. Live from DeFiLlama and CoinGecko, updated hourly." +seo_description: "{{best_name}} has the lowest P/E ratio at {{best_p50}}x among perp DEX tokens. FDV divided by annualized protocol revenue for Hyperliquid, Lighter, Jupiter, GMX, Gains, dYdX, ApeX, SynFutures, Derive, Drift. Live from DeFiLlama and CoinGecko, updated hourly." subtitle: FDV divided by annualized protocol revenue (30-day trailing average) for the major perp DEX tokens. Lower is cheaper relative to earnings. Updated hourly from DeFiLlama and CoinGecko. category: Trading @@ -36,10 +36,11 @@ abstract: | methodology: - "Revenue source: DeFiLlama dailyRevenue (protocol revenue accruing to treasury/holders, not total user fees)." - - "30-day trailing average: last 30 data points from totalDataChart, filtered for non-zero values." - - "Annual revenue: 30d avg x 365." - - "FDV source: CoinGecko /coins/markets fully_diluted_valuation (falls back to market_cap if FDV null)." + - "Annual revenue: sum of the last 30 daily points of totalDataChart x 365/30. Zero days inside the window count, so an adapter gap lowers the run rate instead of hiding it." + - "Revenue scope: the DeFiLlama parent the token accrues from (Jupiter, Hyperliquid, GMX, Lighter, ApeX, Derive span several products), so FDV and revenue share one scope." + - "FDV source: CoinGecko /coins/markets fully_diluted_valuation (falls back to market_cap if FDV null). dYdX is priced on the dYdX Chain token (CoinGecko dydx-chain)." - "P/E = FDV / annual_revenue. Null if FDV or revenue unavailable." + - "Protocols whose adapter publishes no revenue series (Aster, Avantis, Orderly) or reports zero (Drift perps) have no P/E; see bench 265 for their price to fees." - "Poll cadence: harness fetches DeFiLlama + CoinGecko every 60 minutes. Prometheus scrapes :2112/metrics every 60s." findings: @@ -129,3 +130,68 @@ providers: success: perp_protocol_health{protocol="drift"} sample_size: perp_protocol_pe_ratio{protocol="drift"} series: perp_protocol_pe_ratio{protocol="drift"} + + - slug: lighter + name: LIT + tag: "Lighter, parent scope" + formula: "P/E = CoinGecko FDV / (DeFiLlama 30d revenue x 365/30)" + queries: + p50: perp_protocol_pe_ratio{protocol="lighter"} + p90: perp_protocol_pe_ratio{protocol="lighter"} + p99: perp_protocol_pe_ratio{protocol="lighter"} + mean: perp_protocol_pe_ratio{protocol="lighter"} + success: perp_protocol_health{protocol="lighter"} + sample_size: perp_protocol_pe_ratio{protocol="lighter"} + series: perp_protocol_pe_ratio{protocol="lighter"} + + - slug: jupiter + name: JUP + tag: "Jupiter, parent scope: all products" + formula: "P/E = CoinGecko FDV / (DeFiLlama 30d revenue x 365/30)" + queries: + p50: perp_protocol_pe_ratio{protocol="jupiter"} + p90: perp_protocol_pe_ratio{protocol="jupiter"} + p99: perp_protocol_pe_ratio{protocol="jupiter"} + mean: perp_protocol_pe_ratio{protocol="jupiter"} + success: perp_protocol_health{protocol="jupiter"} + sample_size: perp_protocol_pe_ratio{protocol="jupiter"} + series: perp_protocol_pe_ratio{protocol="jupiter"} + + - slug: apex + name: APEX + tag: "ApeX, parent scope" + formula: "P/E = CoinGecko FDV / (DeFiLlama 30d revenue x 365/30)" + queries: + p50: perp_protocol_pe_ratio{protocol="apex"} + p90: perp_protocol_pe_ratio{protocol="apex"} + p99: perp_protocol_pe_ratio{protocol="apex"} + mean: perp_protocol_pe_ratio{protocol="apex"} + success: perp_protocol_health{protocol="apex"} + sample_size: perp_protocol_pe_ratio{protocol="apex"} + series: perp_protocol_pe_ratio{protocol="apex"} + + - slug: synfutures + name: F + tag: "SynFutures" + formula: "P/E = CoinGecko FDV / (DeFiLlama 30d revenue x 365/30)" + queries: + p50: perp_protocol_pe_ratio{protocol="synfutures"} + p90: perp_protocol_pe_ratio{protocol="synfutures"} + p99: perp_protocol_pe_ratio{protocol="synfutures"} + mean: perp_protocol_pe_ratio{protocol="synfutures"} + success: perp_protocol_health{protocol="synfutures"} + sample_size: perp_protocol_pe_ratio{protocol="synfutures"} + series: perp_protocol_pe_ratio{protocol="synfutures"} + + - slug: derive + name: DRV + tag: "Derive, parent scope: perps and options" + formula: "P/E = CoinGecko FDV / (DeFiLlama 30d revenue x 365/30)" + queries: + p50: perp_protocol_pe_ratio{protocol="derive"} + p90: perp_protocol_pe_ratio{protocol="derive"} + p99: perp_protocol_pe_ratio{protocol="derive"} + mean: perp_protocol_pe_ratio{protocol="derive"} + success: perp_protocol_health{protocol="derive"} + sample_size: perp_protocol_pe_ratio{protocol="derive"} + series: perp_protocol_pe_ratio{protocol="derive"} diff --git a/benchmarks/perp-pf-ratio.yml b/benchmarks/perp-pf-ratio.yml new file mode 100644 index 000000000..7ded18255 --- /dev/null +++ b/benchmarks/perp-pf-ratio.yml @@ -0,0 +1,396 @@ +# OpenChainBench. Bench 265 + +slug: perp-pf-ratio +number: "265" +title: "Perp DEX price to fees ratio 2026: Hyperliquid, Aster, Lighter, GMX, GNS live" +seo_title: "Perp DEX P/F ratio 2026: Hyperliquid vs Lighter vs Aster vs GMX vs GNS" +seo_description: "{{best_name}} trades at {{best_p50}}x annualized fees, the cheapest perp DEX token. Market cap and FDV against trailing fees and revenue for {{count}} perpetual DEXes, live from DeFiLlama and CoinGecko." +subtitle: Market cap divided by annualized trading fees (trailing 30 days) for every perp DEX with a listed token, alongside FDV, protocol revenue, float and open interest. Lower means the market pays less per dollar of fees. Same convention as the DeFiLlama P/F ranking, updated hourly. + +category: Trading +status: live +metric: P/F ratio +unit: x +higher_is_better: false + +disclaimer: | + P/F = circulating market cap (CoinGecko) / annualized fees (DeFiLlama dailyFees, trailing 30d x 365/30), on the DeFiLlama parent the token accrues from. Fees include the share paid to liquidity providers; P/S uses protocol revenue only. Low-float tokens show a much higher FDV/F than P/F. Venues without a token publish fees and OI, no ratio. A low multiple is not a recommendation. + +seo_intro: | + The price to fees ratio for perp DEX tokens measures how many years of current trading fees + the market is paying for. It is the on-chain cousin of a price to sales multiple: market cap on + top, annualized fees at the bottom. A low P/F means the token is cheap relative to the fees + its venue generates; a high P/F prices in growth, or a token that does not capture the fees. + Hyperliquid, Aster and Lighter generate the most absolute fees but carry multi-billion + valuations with a quarter of supply in circulation, so their FDV/F runs far above their P/F. + GMX and Gains are fully circulating with real fee flow and trade at low single-digit + multiples. dYdX and Derive sit at the expensive end on today's fee run rate. This benchmark + tracks the whole table live from DeFiLlama (fees, revenue, open interest) and CoinGecko + (market cap, FDV, supply), the same inputs as defillama.com/pf, cut identically for every + protocol and refreshed every hour. + +abstract: | + The perp-token-metrics harness polls DeFiLlama /summary/fees/[slug] for dailyFees and + dailyRevenue every hour and cuts the trailing 30, 60 and 365 day windows itself from + totalDataChart, so every protocol is annualized the same way (30d sum x 365/30) regardless + of which convenience fields its adapter fills. It fetches market cap, fully diluted valuation + and circulating vs total supply from CoinGecko /coins/markets in one batch, and open interest + from the DeFiLlama open-interest overview. P/F = mcap / annualized fees, FDV/F = FDV / annualized + fees, P/S = mcap / annualized revenue. Ratios whose denominator is zero or whose token has no + listing are not published, so a missing value never reads as a zero valuation. Gauges are + perp_protocol_pf_ratio, perp_protocol_pf_fdv_ratio, perp_protocol_ps_ratio, + perp_protocol_fees_30d_usd, perp_protocol_rev_30d_usd, perp_protocol_mcap_usd, + perp_protocol_fdv_usd, perp_protocol_float_pct and perp_protocol_oi_usd, one series per protocol. + +methodology: + - "Fees source: DeFiLlama dailyFees for the token's parent entity (all products the token accrues from), so market cap and fees share one scope. Perps-only fees are published separately as perp_protocol_perp_fees_30d_usd." + - "Annualized fees: sum of the last 30 daily points x 365/30. Zero days inside the window count; the current partial day is included, as in DeFiLlama's own 30d figure." + - "Market cap and FDV: CoinGecko /coins/markets circulating market cap and fully_diluted_valuation (market cap when FDV is null). Float = circulating / total supply." + - "P/F = market cap / annualized fees (DeFiLlama P/F convention). FDV/F = FDV / annualized fees. P/S = market cap / annualized protocol revenue (DeFiLlama dailyRevenue). Bench 234 keeps P/E = FDV / annualized revenue." + - "Revenue share = 30d revenue / 30d fees. Adapters that publish no revenue series, or a flat zero, leave P/S and revenue share undefined rather than zero." + - "Open interest: DeFiLlama open-interest overview, perps product only. Fees to OI = annualized fees / open interest." + - "Drift's DeFiLlama perps adapter (drift-trade) currently reports zero fees; the row stays in the cohort with every ratio undefined until the source recovers." + - "Rows without a P/F (no token, or no fees) read their success from the P/F gauge itself, so they rank as unavailable on the headline and are excluded from the best / median / worst strip while still appearing in the fees and open interest tabs." + - "Poll cadence: DeFiLlama and CoinGecko every 60 minutes. Prometheus scrapes :2112/metrics every 60s." + +findings: + - "{{best_name}} is the cheapest perp DEX token at {{best_p50}}x annualized fees." + - "Hyperliquid, Aster and Lighter earn the most fees in absolute terms but trade at a large multiple, and their FDV/F is three to four times their P/F because most supply is not circulating." + - "GMX and Gains are fully circulating, so their P/F and FDV/F coincide; both sit in low single digits." + - "{{worst_name}} is the most expensive token in the cohort at {{worst_p50}}x on today's fee run rate." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-token-metrics + +prometheus: + window: 24h + freshness_metric: perp_protocol_health + +metric_panels: + - id: pf_fdv + label: FDV / fees + label_key: protocol + metric: perp_protocol_pf_fdv_ratio + unit: x + description: "Fully diluted valuation divided by annualized fees. The conservative multiple: it prices unvested supply as if it were circulating." + - id: ps + label: P/S + label_key: protocol + metric: perp_protocol_ps_ratio + unit: x + description: "Market cap divided by annualized protocol revenue (the share of fees that reaches the protocol, treasury or token holders)." + - id: fees_30d + label: Fees 30d + label_key: protocol + metric: perp_protocol_fees_30d_usd + unit: usd + higher_is_better: true + description: "Trailing 30 day fees paid by traders, token scope (parent protocol). Includes venues without a token." + - id: rev_30d + label: Revenue 30d + label_key: protocol + metric: perp_protocol_rev_30d_usd + unit: usd + higher_is_better: true + description: "Trailing 30 day protocol revenue per DeFiLlama dailyRevenue. Absent when the adapter publishes fees only." + - id: rev_share + label: Revenue share + label_key: protocol + metric: perp_protocol_rev_share_pct + unit: pct + higher_is_better: true + tab: false + description: "Share of 30 day fees retained as protocol revenue." + - id: mcap + label: Market cap + label_key: protocol + metric: perp_protocol_mcap_usd + unit: usd + tab: false + description: "Circulating market cap from CoinGecko." + - id: fdv + label: FDV + label_key: protocol + metric: perp_protocol_fdv_usd + unit: usd + tab: false + description: "Fully diluted valuation from CoinGecko." + - id: float_pct + label: Float + label_key: protocol + metric: perp_protocol_float_pct + unit: pct + higher_is_better: true + description: "Circulating supply as a percent of total supply. The gap between P/F and FDV/F is this number." + - id: oi + label: Open interest + label_key: protocol + metric: perp_protocol_oi_usd + unit: usd + higher_is_better: true + description: "Open interest on the perps product from the DeFiLlama open-interest overview." + - id: fees_to_oi + label: Fees / OI + label_key: protocol + metric: perp_protocol_fees_to_oi_ratio + unit: x + higher_is_better: true + description: "Annualized fees divided by open interest: how much fee the venue extracts per dollar of positions held. High values flag either high take rates or churned, possibly incentivized, volume." + +ledger_columns: + - { label: "P/F", slot: p50 } + - { label: "FDV / fees", panel: pf_fdv } + - { label: "P/S", panel: ps } + - { label: "Fees 30d", panel: fees_30d } + - { label: "Market cap", panel: mcap } + - { label: "Float", panel: float_pct } + +faq: + - q: "What is the price to fees ratio of a perp DEX?" + a: "Market cap divided by the fees the venue generates in a year, using the trailing 30 days annualized. It answers how many years of current fee flow the market is paying for. It is the same P/F that defillama.com/pf ranks, computed here per protocol with identical windows and published as a Prometheus series you can query." + - q: "Why show both P/F and FDV/F?" + a: "P/F uses circulating market cap, so a token with 25 percent of supply in circulation looks four times cheaper than the same token valued on fully diluted supply. FDV/F prices unvested allocations as if they were already trading. Neither is wrong; the float column tells you how far apart they are for each token." + - q: "How do fees differ from revenue here?" + a: "Fees are everything traders paid, including the share that goes to liquidity providers, vaults or referrers. Revenue is the part that reaches the protocol, its treasury or token holders. Hyperliquid routes almost all fees to HYPE buybacks so its P/F and P/S are close; GMX passes most fees to GLP and GM depositors, so its P/S is well above its P/F." + - q: "Why does Jupiter's fee number include more than perps?" + a: "JUP is one token for the aggregator, the perps exchange, lending and launchpad. Comparing its market cap against perps fees alone would overstate the multiple. The bench measures every token against the fees of the DeFiLlama parent it accrues from and publishes the perps-only figure separately." + - q: "Why are Ostium, Pacifica, Extended, edgeX and Paradex in the table without a ratio?" + a: "They have no listed token, so there is no market cap to divide. Their fees and open interest are published so the cohort shows the whole perp DEX fee landscape, not only the venues with a token." + - q: "Is a low P/F a buy signal?" + a: "No. It can mean the market expects fees to fall, that fees were inflated by incentivized or wash volume, or that the token has no claim on the fees. Read it with the float, the revenue share and the month over month fee trend, then with your own research." + - q: "How is this different from bench 234, the perp DEX P/E ratio?" + a: "Bench 234 divides FDV by protocol revenue: the strictest multiple, on the money that actually reaches holders. This bench divides market cap by total fees, the broader and more common convention. The same harness computes both from the same inputs every hour." + +providers: + - slug: hyperliquid + name: HYPE + tag: "Hyperliquid, parent scope: perps, spot, HLP" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="hyperliquid"} + p90: perp_protocol_pf_ratio{protocol="hyperliquid"} + p99: perp_protocol_pf_ratio{protocol="hyperliquid"} + mean: perp_protocol_pf_ratio{protocol="hyperliquid"} + success: perp_protocol_health{protocol="hyperliquid"} + sample_size: perp_protocol_fees_30d_usd{protocol="hyperliquid"} + series: perp_protocol_pf_ratio{protocol="hyperliquid"} + + - slug: aster + name: ASTER + tag: "Aster, BNB Chain perps" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="aster"} + p90: perp_protocol_pf_ratio{protocol="aster"} + p99: perp_protocol_pf_ratio{protocol="aster"} + mean: perp_protocol_pf_ratio{protocol="aster"} + success: perp_protocol_health{protocol="aster"} + sample_size: perp_protocol_fees_30d_usd{protocol="aster"} + series: perp_protocol_pf_ratio{protocol="aster"} + + - slug: lighter + name: LIT + tag: "Lighter, parent scope: Lighter and Robinhood perps" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="lighter"} + p90: perp_protocol_pf_ratio{protocol="lighter"} + p99: perp_protocol_pf_ratio{protocol="lighter"} + mean: perp_protocol_pf_ratio{protocol="lighter"} + success: perp_protocol_health{protocol="lighter"} + sample_size: perp_protocol_fees_30d_usd{protocol="lighter"} + series: perp_protocol_pf_ratio{protocol="lighter"} + + - slug: jupiter + name: JUP + tag: "Jupiter, parent scope: all Jupiter products" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="jupiter"} + p90: perp_protocol_pf_ratio{protocol="jupiter"} + p99: perp_protocol_pf_ratio{protocol="jupiter"} + mean: perp_protocol_pf_ratio{protocol="jupiter"} + success: perp_protocol_health{protocol="jupiter"} + sample_size: perp_protocol_fees_30d_usd{protocol="jupiter"} + series: perp_protocol_pf_ratio{protocol="jupiter"} + + - slug: gmx + name: GMX + tag: "GMX, parent scope: GMX v1 and v2" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="gmx"} + p90: perp_protocol_pf_ratio{protocol="gmx"} + p99: perp_protocol_pf_ratio{protocol="gmx"} + mean: perp_protocol_pf_ratio{protocol="gmx"} + success: perp_protocol_health{protocol="gmx"} + sample_size: perp_protocol_fees_30d_usd{protocol="gmx"} + series: perp_protocol_pf_ratio{protocol="gmx"} + + - slug: gains + name: GNS + tag: "Gains Network, gTrade, multi-chain" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="gains"} + p90: perp_protocol_pf_ratio{protocol="gains"} + p99: perp_protocol_pf_ratio{protocol="gains"} + mean: perp_protocol_pf_ratio{protocol="gains"} + success: perp_protocol_health{protocol="gains"} + sample_size: perp_protocol_fees_30d_usd{protocol="gains"} + series: perp_protocol_pf_ratio{protocol="gains"} + + - slug: dydx + name: DYDX + tag: "dYdX, dYdX chain" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="dydx"} + p90: perp_protocol_pf_ratio{protocol="dydx"} + p99: perp_protocol_pf_ratio{protocol="dydx"} + mean: perp_protocol_pf_ratio{protocol="dydx"} + success: perp_protocol_health{protocol="dydx"} + sample_size: perp_protocol_fees_30d_usd{protocol="dydx"} + series: perp_protocol_pf_ratio{protocol="dydx"} + + - slug: drift + name: DRIFT + tag: "Drift, Solana; DeFiLlama perps adapter reports zero" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="drift"} + p90: perp_protocol_pf_ratio{protocol="drift"} + p99: perp_protocol_pf_ratio{protocol="drift"} + mean: perp_protocol_pf_ratio{protocol="drift"} + success: perp_protocol_pf_ratio{protocol="drift"} + sample_size: perp_protocol_fees_30d_usd{protocol="drift"} + series: perp_protocol_pf_ratio{protocol="drift"} + + - slug: avantis + name: AVNT + tag: "Avantis, Base perps" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="avantis"} + p90: perp_protocol_pf_ratio{protocol="avantis"} + p99: perp_protocol_pf_ratio{protocol="avantis"} + mean: perp_protocol_pf_ratio{protocol="avantis"} + success: perp_protocol_health{protocol="avantis"} + sample_size: perp_protocol_fees_30d_usd{protocol="avantis"} + series: perp_protocol_pf_ratio{protocol="avantis"} + + - slug: apex + name: APEX + tag: "ApeX, parent scope: ApeX Omni and Pro" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="apex"} + p90: perp_protocol_pf_ratio{protocol="apex"} + p99: perp_protocol_pf_ratio{protocol="apex"} + mean: perp_protocol_pf_ratio{protocol="apex"} + success: perp_protocol_health{protocol="apex"} + sample_size: perp_protocol_fees_30d_usd{protocol="apex"} + series: perp_protocol_pf_ratio{protocol="apex"} + + - slug: orderly + name: ORDER + tag: "Orderly, omnichain perps" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="orderly"} + p90: perp_protocol_pf_ratio{protocol="orderly"} + p99: perp_protocol_pf_ratio{protocol="orderly"} + mean: perp_protocol_pf_ratio{protocol="orderly"} + success: perp_protocol_health{protocol="orderly"} + sample_size: perp_protocol_fees_30d_usd{protocol="orderly"} + series: perp_protocol_pf_ratio{protocol="orderly"} + + - slug: synfutures + name: F + tag: "SynFutures, parent scope: v3" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="synfutures"} + p90: perp_protocol_pf_ratio{protocol="synfutures"} + p99: perp_protocol_pf_ratio{protocol="synfutures"} + mean: perp_protocol_pf_ratio{protocol="synfutures"} + success: perp_protocol_health{protocol="synfutures"} + sample_size: perp_protocol_fees_30d_usd{protocol="synfutures"} + series: perp_protocol_pf_ratio{protocol="synfutures"} + + - slug: derive + name: DRV + tag: "Derive, parent scope: perps and options" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" + queries: + p50: perp_protocol_pf_ratio{protocol="derive"} + p90: perp_protocol_pf_ratio{protocol="derive"} + p99: perp_protocol_pf_ratio{protocol="derive"} + mean: perp_protocol_pf_ratio{protocol="derive"} + success: perp_protocol_health{protocol="derive"} + sample_size: perp_protocol_fees_30d_usd{protocol="derive"} + series: perp_protocol_pf_ratio{protocol="derive"} + + - slug: ostium + name: Ostium + tag: "Ostium, no token: fees and OI only" + formula: "No listed token; fees and open interest published, no ratio" + queries: + p50: perp_protocol_pf_ratio{protocol="ostium"} + p90: perp_protocol_pf_ratio{protocol="ostium"} + p99: perp_protocol_pf_ratio{protocol="ostium"} + mean: perp_protocol_pf_ratio{protocol="ostium"} + success: perp_protocol_pf_ratio{protocol="ostium"} + sample_size: perp_protocol_fees_30d_usd{protocol="ostium"} + series: perp_protocol_pf_ratio{protocol="ostium"} + + - slug: pacifica + name: Pacifica + tag: "Pacifica, no token: fees and OI only" + formula: "No listed token; fees and open interest published, no ratio" + queries: + p50: perp_protocol_pf_ratio{protocol="pacifica"} + p90: perp_protocol_pf_ratio{protocol="pacifica"} + p99: perp_protocol_pf_ratio{protocol="pacifica"} + mean: perp_protocol_pf_ratio{protocol="pacifica"} + success: perp_protocol_pf_ratio{protocol="pacifica"} + sample_size: perp_protocol_fees_30d_usd{protocol="pacifica"} + series: perp_protocol_pf_ratio{protocol="pacifica"} + + - slug: extended + name: Extended + tag: "Extended, no token: fees and OI only" + formula: "No listed token; fees and open interest published, no ratio" + queries: + p50: perp_protocol_pf_ratio{protocol="extended"} + p90: perp_protocol_pf_ratio{protocol="extended"} + p99: perp_protocol_pf_ratio{protocol="extended"} + mean: perp_protocol_pf_ratio{protocol="extended"} + success: perp_protocol_pf_ratio{protocol="extended"} + sample_size: perp_protocol_fees_30d_usd{protocol="extended"} + series: perp_protocol_pf_ratio{protocol="extended"} + + - slug: edgex + name: edgeX + tag: "edgeX, no token: fees and OI only" + formula: "No listed token; fees and open interest published, no ratio" + queries: + p50: perp_protocol_pf_ratio{protocol="edgex"} + p90: perp_protocol_pf_ratio{protocol="edgex"} + p99: perp_protocol_pf_ratio{protocol="edgex"} + mean: perp_protocol_pf_ratio{protocol="edgex"} + success: perp_protocol_pf_ratio{protocol="edgex"} + sample_size: perp_protocol_fees_30d_usd{protocol="edgex"} + series: perp_protocol_pf_ratio{protocol="edgex"} + + - slug: paradex + name: Paradex + tag: "Paradex, no token: fees and OI only" + formula: "No listed token; fees and open interest published, no ratio" + queries: + p50: perp_protocol_pf_ratio{protocol="paradex"} + p90: perp_protocol_pf_ratio{protocol="paradex"} + p99: perp_protocol_pf_ratio{protocol="paradex"} + mean: perp_protocol_pf_ratio{protocol="paradex"} + success: perp_protocol_pf_ratio{protocol="paradex"} + sample_size: perp_protocol_fees_30d_usd{protocol="paradex"} + series: perp_protocol_pf_ratio{protocol="paradex"} diff --git a/harnesses/perp-token-metrics/README.md b/harnesses/perp-token-metrics/README.md new file mode 100644 index 000000000..697d9d240 --- /dev/null +++ b/harnesses/perp-token-metrics/README.md @@ -0,0 +1,79 @@ +# perp-token-metrics + +Hourly valuation snapshot for perp DEX tokens. Feeds two benches: + +- **234 `perp-pe-ratio`**: FDV / annualized protocol revenue. +- **265 `perp-pf-ratio`**: market cap / annualized fees, plus FDV/F, P/S, float, open interest. + +## Inputs + +| Source | Endpoint | What we take | +|---|---|---| +| DeFiLlama | `GET /summary/fees/{slug}?dataType=dailyFees` | `total24h`, `totalDataChart` (we cut 30d / days 31-60 / 365d ourselves) | +| DeFiLlama | `GET /summary/fees/{slug}?dataType=dailyRevenue` | same, revenue. Flat-zero or missing series = "no revenue data", not zero | +| DeFiLlama | `GET /overview/open-interest` | `protocols[].total24h` keyed by perps child slug | +| CoinGecko | `GET /coins/markets?ids=…` | `market_cap`, `fully_diluted_valuation` (falls back to mcap), `circulating_supply`, `total_supply` | + +`/summary/fees/{parent}` aggregates every child product (verified: `hyperliquid` = perps + spot + HLP, `jupiter` = perps + aggregator + lend). The token is measured against its parent so market cap and fees share one scope; the perps-only child is published as `perp_protocol_perp_fees_30d_usd`. + +DeFiLlama's derivatives *volume* summary is paid-tier; volume is not read here (the perp-cohort-stats harness publishes `perp_venue_volume_30d_usd`). + +## Formulas + +``` +annual(x) = sum(last 30 daily points) * 365 / 30 +P/F = mcap / annual(fees) +FDV/F = fdv / annual(fees) +P/S = mcap / annual(revenue) +P/E = fdv / annual(revenue) (bench 234) +rev share = 100 * rev30d / fees30d +float % = 100 * circulating / total supply +fees / OI = annual(fees) / open interest +``` + +A ratio whose denominator is zero, or whose token has no listing, is **deleted** from the gauge vector rather than set to 0, so a missing value never reads as a zero valuation. + +## Coverage (verified 2026-09-12) + +| slug | DeFiLlama (token scope) | perps child | CoinGecko | notes | +|---|---|---|---|---| +| hyperliquid | hyperliquid | hyperliquid-perps | hyperliquid | | +| gmx | gmx | gmx-v2-perps | gmx | | +| gains | gains-network | = | gains-network | 100 % float | +| dydx | dydx | dydx-v4 | dydx-chain | | +| drift | drift-trade | = | drift-protocol | adapter reports 0 fees; ratios undefined | +| jupiter | jupiter | jupiter-perpetual-exchange | jupiter-exchange-solana | perps ≈ half of parent fees | +| aster | aster | aster-perps | aster-2 | no revenue series | +| lighter | lighter | lighter-perps | lighter | parent adds Robinhood perps | +| avantis | avantis | = | avantis | no revenue series | +| apex | apex-protocol | apex-omni | apex-token-2 | | +| orderly | orderly-perps | = | orderly-network | parent slug returns null; no revenue | +| synfutures | synfutures | synfutures-v3 | synfutures | | +| derive | derive | derive-v2 | derive | parent adds options | +| ostium, pacifica, extended, edgex, paradex | see main.go | | none | fees + OI only | + +Dropped: vertex (no data since acquisition), aevo / grvt / backpack (no DeFiLlama fees series), mux / perpetual-protocol (fees ≈ 0). + +## Metrics + +All gauges carry `protocol=""`. + +``` +perp_protocol_pf_ratio perp_protocol_fees_24h_usd perp_protocol_mcap_usd +perp_protocol_pf_fdv_ratio perp_protocol_fees_30d_usd perp_protocol_fdv_usd +perp_protocol_ps_ratio perp_protocol_fees_prev_30d_usd perp_protocol_float_pct +perp_protocol_pe_ratio perp_protocol_fees_1y_usd perp_protocol_oi_usd +perp_protocol_rev_share_pct perp_protocol_annual_fees_usd perp_protocol_fees_to_oi_ratio +perp_protocol_rev_24h_usd perp_protocol_perp_fees_30d_usd perp_protocol_health +perp_protocol_rev_30d_usd perp_protocol_annual_rev_usd +``` + +## Run + +``` +go test ./... # unit tests, fake upstream +ONESHOT=1 METRICS_ADDR=:2112 go run ./cmd/script # one live poll, then exit +curl -s localhost:2112/metrics | grep perp_protocol_pf_ratio +``` + +~40 DeFiLlama calls and one CoinGecko call per hour; both public tiers allow this comfortably. diff --git a/harnesses/perp-token-metrics/cmd/script/fetcher.go b/harnesses/perp-token-metrics/cmd/script/fetcher.go index 2ec5a9d26..4a572ad58 100644 --- a/harnesses/perp-token-metrics/cmd/script/fetcher.go +++ b/harnesses/perp-token-metrics/cmd/script/fetcher.go @@ -4,12 +4,19 @@ import ( "encoding/json" "fmt" "net/http" + "sort" "strings" "time" ) var httpClient = &http.Client{Timeout: 30 * time.Second} +// Base URLs are variables so tests can point them at an httptest server. +var ( + llamaBase = "https://api.llama.fi" + cgBase = "https://api.coingecko.com/api/v3" +) + func get(url string) (*http.Response, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -19,12 +26,28 @@ func get(url string) (*http.Response, error) { return httpClient.Do(req) } -// fetchLlamaRevenue returns (rev24h, avg30dDaily, ok). -func fetchLlamaRevenue(slug string) (float64, float64, bool) { - url := fmt.Sprintf("https://api.llama.fi/summary/fees/%s?dataType=dailyRevenue", slug) +// llamaSeries is what we keep from one /summary/fees/?dataType=… +// response: the headline 24h number and the trailing windows we derive +// from totalDataChart ourselves, so every protocol is cut the same way +// regardless of which convenience fields the adapter happens to fill. +type llamaSeries struct { + Total24h float64 + Sum30d float64 // last 30 daily points + SumPrev30 float64 // the 30 points before those (days 31-60) + Sum1y float64 // last 365 daily points + Avg30d float64 // Sum30d / non-zero days in the window + Days30 int // non-zero days counted in Avg30d +} + +// fetchLlamaSeries returns the trailing windows for one DeFiLlama slug and +// data type ("dailyFees" or "dailyRevenue"). ok is false on transport or +// decode failure, or when the chart is empty; a chart of zeros is ok=true +// with zero sums, which is a real observation (adapter live, nothing earned). +func fetchLlamaSeries(slug, dataType string) (llamaSeries, bool) { + url := fmt.Sprintf("%s/summary/fees/%s?dataType=%s", llamaBase, slug, dataType) resp, err := get(url) if err != nil || resp.StatusCode != 200 { - return 0, 0, false + return llamaSeries{}, false } defer resp.Body.Close() @@ -33,40 +56,74 @@ func fetchLlamaRevenue(slug string) (float64, float64, bool) { TotalDataChart [][2]json.Number `json:"totalDataChart"` } if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { - return 0, 0, false + return llamaSeries{}, false + } + if len(d.TotalDataChart) == 0 { + return llamaSeries{}, false } + return windowsFromChart(d.Total24h, d.TotalDataChart), true +} - chart := d.TotalDataChart - start := len(chart) - 30 - if start < 0 { - start = 0 - } - var sum float64 - var count int - for _, pt := range chart[start:] { - v, err := pt[1].Float64() - if err != nil || v <= 0 { +// windowsFromChart cuts the trailing windows out of a DeFiLlama +// totalDataChart ([[unixDay, value], …]). Points are sorted by timestamp +// first because the API does not promise order. The current partial day +// is not special-cased: DeFiLlama's own 30d figure includes it too. +func windowsFromChart(total24h float64, chart [][2]json.Number) llamaSeries { + type pt struct { + t int64 + v float64 + } + pts := make([]pt, 0, len(chart)) + for _, c := range chart { + t, err1 := c[0].Int64() + v, err2 := c[1].Float64() + if err1 != nil || err2 != nil { continue } - sum += v - count++ + pts = append(pts, pt{t, v}) + } + sort.Slice(pts, func(i, j int) bool { return pts[i].t < pts[j].t }) + + s := llamaSeries{Total24h: total24h} + n := len(pts) + for i := n - 1; i >= 0 && i >= n-365; i-- { + v := pts[i].v + s.Sum1y += v + back := n - 1 - i // 0 = latest day + switch { + case back < 30: + s.Sum30d += v + if v > 0 { + s.Days30++ + } + case back < 60: + s.SumPrev30 += v + } } - if count == 0 { - return d.Total24h, 0, false + if s.Days30 > 0 { + s.Avg30d = s.Sum30d / float64(s.Days30) } - return d.Total24h, sum / float64(count), true + return s } -// fetchCGFdvBatch fetches FDV for multiple CoinGecko IDs in one request. -func fetchCGFdvBatch(ids []string) map[string]float64 { - result := map[string]float64{} +// cgMarket is the slice of CoinGecko /coins/markets we use. +type cgMarket struct { + Mcap float64 + FDV float64 + Circ float64 + TotalSupply float64 +} + +// fetchCGMarkets fetches market cap, FDV and supply for many CoinGecko IDs +// in one request. Missing IDs are simply absent from the map. +func fetchCGMarkets(ids []string) map[string]cgMarket { + result := map[string]cgMarket{} if len(ids) == 0 { return result } - url := fmt.Sprintf( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=%s&per_page=50&sparkline=false", - strings.Join(ids, ","), + "%s/coins/markets?vs_currency=usd&ids=%s&per_page=100&sparkline=false", + cgBase, strings.Join(ids, ","), ) resp, err := get(url) if err != nil || resp.StatusCode != 200 { @@ -75,21 +132,64 @@ func fetchCGFdvBatch(ids []string) map[string]float64 { defer resp.Body.Close() var coins []struct { - ID string `json:"id"` - FullyDilutedVal *float64 `json:"fully_diluted_valuation"` - MarketCap *float64 `json:"market_cap"` + ID string `json:"id"` + MarketCap *float64 `json:"market_cap"` + FullyDilutedVal *float64 `json:"fully_diluted_valuation"` + Circulating *float64 `json:"circulating_supply"` + TotalSupply *float64 `json:"total_supply"` } if err := json.NewDecoder(resp.Body).Decode(&coins); err != nil { return result } - + deref := func(p *float64) float64 { + if p == nil { + return 0 + } + return *p + } for _, c := range coins { - fdv := c.FullyDilutedVal - if fdv == nil { - fdv = c.MarketCap + m := cgMarket{ + Mcap: deref(c.MarketCap), + FDV: deref(c.FullyDilutedVal), + Circ: deref(c.Circulating), + TotalSupply: deref(c.TotalSupply), + } + // CoinGecko leaves FDV null when total supply is unknown; the + // existing P/E bench fell back to mcap and we keep that. + if m.FDV == 0 { + m.FDV = m.Mcap } - if fdv != nil && *fdv > 0 { - result[c.ID] = *fdv + if m.Mcap > 0 || m.FDV > 0 { + result[c.ID] = m + } + } + return result +} + +// fetchLlamaOpenInterest returns current open interest in USD keyed by +// DeFiLlama perps slug (the overview lists child protocols only). An +// empty map on failure just leaves the OI-derived gauges unset. +func fetchLlamaOpenInterest() map[string]float64 { + result := map[string]float64{} + url := llamaBase + "/overview/open-interest?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true" + resp, err := get(url) + if err != nil || resp.StatusCode != 200 { + return result + } + defer resp.Body.Close() + + var d struct { + Protocols []struct { + Slug string `json:"slug"` + Total24h *float64 `json:"total24h"` + } `json:"protocols"` + } + if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { + return result + } + for _, p := range d.Protocols { + if p.Total24h != nil && *p.Total24h > 0 { + result[p.Slug] = *p.Total24h } } return result diff --git a/harnesses/perp-token-metrics/cmd/script/main.go b/harnesses/perp-token-metrics/cmd/script/main.go index 1eb747f1c..649abe83d 100644 --- a/harnesses/perp-token-metrics/cmd/script/main.go +++ b/harnesses/perp-token-metrics/cmd/script/main.go @@ -8,18 +8,54 @@ import ( "time" ) +// protocol is one row of the valuation table. llamaSlug is the DeFiLlama +// entity the token accrues value from (the parent when the protocol has +// several products, so mcap and fees are measured on the same scope, the +// way defillama.com/pf does it). llamaPerpSlug is the perps-only child, +// used for the perp-share column and the open-interest join; empty means +// the parent is perps-only. cgID empty means the protocol has no listed +// token: fees are still published, every valuation ratio is skipped. type protocol struct { - slug string // bench slug / prometheus label - llamaSlug string // DeFiLlama fees slug - cgID string // CoinGecko coin ID (empty = no FDV) + slug string // bench slug / prometheus label + llamaSlug string // DeFiLlama fees slug, token scope (parent) + llamaPerpSlug string // DeFiLlama fees slug, perps-only child ("" = same as llamaSlug) + cgID string // CoinGecko coin ID ("" = no token) } +// Verified against api.llama.fi/summary/fees/ and CoinGecko +// /coins/markets on 2026-09-12 (see harness README for the coverage +// table). Slugs that DeFiLlama no longer serves (vertex, aevo, grvt, +// backpack) are deliberately absent rather than left to fail every hour. var protocols = []protocol{ - {slug: "hyperliquid", llamaSlug: "hyperliquid", cgID: "hyperliquid"}, - {slug: "gmx", llamaSlug: "gmx", cgID: "gmx"}, + {slug: "hyperliquid", llamaSlug: "hyperliquid", llamaPerpSlug: "hyperliquid-perps", cgID: "hyperliquid"}, + {slug: "gmx", llamaSlug: "gmx", llamaPerpSlug: "gmx-v2-perps", cgID: "gmx"}, {slug: "gains", llamaSlug: "gains-network", cgID: "gains-network"}, - {slug: "dydx", llamaSlug: "dydx", cgID: "dydx"}, - {slug: "drift", llamaSlug: "drift", cgID: "drift-protocol"}, + {slug: "dydx", llamaSlug: "dydx", llamaPerpSlug: "dydx-v4", cgID: "dydx-chain"}, + // Drift's DeFiLlama parent aggregates the staked-SOL product; the perps + // adapter (drift-trade) is what the token is priced on, even while it + // reports zero and leaves every ratio undefined. + {slug: "drift", llamaSlug: "drift-trade", cgID: "drift-protocol"}, + {slug: "jupiter", llamaSlug: "jupiter", llamaPerpSlug: "jupiter-perpetual-exchange", cgID: "jupiter-exchange-solana"}, + {slug: "aster", llamaSlug: "aster", llamaPerpSlug: "aster-perps", cgID: "aster-2"}, + {slug: "lighter", llamaSlug: "lighter", llamaPerpSlug: "lighter-perps", cgID: "lighter"}, + {slug: "avantis", llamaSlug: "avantis", cgID: "avantis"}, + {slug: "apex", llamaSlug: "apex-protocol", llamaPerpSlug: "apex-omni", cgID: "apex-token-2"}, + {slug: "orderly", llamaSlug: "orderly-perps", cgID: "orderly-network"}, + {slug: "synfutures", llamaSlug: "synfutures", llamaPerpSlug: "synfutures-v3", cgID: "synfutures"}, + {slug: "derive", llamaSlug: "derive", llamaPerpSlug: "derive-v2", cgID: "derive"}, + // Token-less venues: fees and open interest only. + {slug: "ostium", llamaSlug: "ostium"}, + {slug: "pacifica", llamaSlug: "pacifica", llamaPerpSlug: "pacifica-perps"}, + {slug: "extended", llamaSlug: "extended", llamaPerpSlug: "extended-perps"}, + {slug: "edgex", llamaSlug: "edgex", llamaPerpSlug: "edgex-v2"}, + {slug: "paradex", llamaSlug: "paradex-perps"}, +} + +func (p protocol) perpSlug() string { + if p.llamaPerpSlug != "" { + return p.llamaPerpSlug + } + return p.llamaSlug } func addr() string { @@ -31,7 +67,7 @@ func addr() string { func main() { fmt.Println("=== perp-token-metrics harness ===") - fmt.Println("OpenChainBench #234 — perp DEX token P/E ratios") + fmt.Println("OpenChainBench #234 (P/E) + perp DEX valuation (P/F, P/S)") fmt.Printf("Protocols: %d | poll interval: 1h\n", len(protocols)) go func() { @@ -43,6 +79,10 @@ func main() { poll() + if os.Getenv("ONESHOT") == "1" { + return + } + ticker := time.NewTicker(60 * time.Minute) defer ticker.Stop() @@ -63,42 +103,74 @@ func main() { func poll() { fmt.Printf("[%s] polling DeFiLlama + CoinGecko\n", time.Now().UTC().Format(time.RFC3339)) - // collect CoinGecko FDVs in one batch call + // One batch call for every token, one call for every open-interest row. cgIDs := []string{} for _, p := range protocols { if p.cgID != "" { cgIDs = append(cgIDs, p.cgID) } } - fdvMap := fetchCGFdvBatch(cgIDs) + markets := fetchCGMarkets(cgIDs) + oi := fetchLlamaOpenInterest() for _, p := range protocols { - rev24h, avg30d, ok := fetchLlamaRevenue(p.llamaSlug) - if !ok { - fmt.Printf(" [%s] DeFiLlama fetch failed\n", p.slug) + fees, feesOK := fetchLlamaSeries(p.llamaSlug, "dailyFees") + if !feesOK { + fmt.Printf(" [%s] DeFiLlama fees fetch failed\n", p.slug) protocolHealth.WithLabelValues(p.slug).Set(0) continue } - - annualRev := avg30d * 365 - fdv, hasFDV := fdvMap[p.cgID] - var pe float64 - hasPE := hasFDV && annualRev > 0 - if hasPE { - pe = fdv / annualRev + // Revenue is optional: several adapters publish fees only, and an + // adapter that answers with a flat zero series is the same thing. + rev, revOK := fetchLlamaSeries(p.llamaSlug, "dailyRevenue") + revOK = revOK && rev.Sum30d > 0 + perpFees := fees + if p.perpSlug() != p.llamaSlug { + if pf, ok := fetchLlamaSeries(p.perpSlug(), "dailyFees"); ok { + perpFees = pf + } } - protocolRev24h.WithLabelValues(p.slug).Set(rev24h) - protocolAnnualRev.WithLabelValues(p.slug).Set(annualRev) - if hasFDV { - protocolFDV.WithLabelValues(p.slug).Set(fdv) - } - if hasPE { - protocolPE.WithLabelValues(p.slug).Set(pe) - } + v := computeValuation(fees, rev, revOK, markets[p.cgID], p.cgID != "") + publish(p, v, perpFees, oi[p.perpSlug()]) protocolHealth.WithLabelValues(p.slug).Set(1) - fmt.Printf(" [%s] rev24h=$%.0f avg30d=$%.0f annual=$%.0f fdv=$%.0f pe=%.1fx\n", - p.slug, rev24h, avg30d, annualRev, fdv, pe) + fmt.Printf(" [%s] fees30d=$%.0f rev30d=$%.0f mcap=$%.0f fdv=$%.0f pf=%.1fx ps=%.1fx pe=%.1fx\n", + p.slug, fees.Sum30d, rev.Sum30d, v.Mcap, v.FDV, v.PF, v.PS, v.PE) + } +} + +// publish writes one protocol's row into the gauge vectors. Ratios that +// could not be computed (no token, zero fees) are removed from the vector +// so a stale value from a previous poll never survives a source outage. +func publish(p protocol, v valuation, perpFees llamaSeries, oiUSD float64) { + l := labelsFor(p.slug) + + protocolFees24h.With(l).Set(v.Fees24h) + protocolFees30d.With(l).Set(v.Fees30d) + protocolFeesPrev30d.With(l).Set(v.FeesPrev30d) + protocolFees1y.With(l).Set(v.Fees1y) + protocolAnnualFees.With(l).Set(v.AnnualFees) + protocolPerpFees30d.With(l).Set(perpFees.Sum30d) + + setOrDelete(protocolRev24h, l, v.Rev24h, v.HasRev) + setOrDelete(protocolRev30d, l, v.Rev30d, v.HasRev) + setOrDelete(protocolAnnualRev, l, v.AnnualRev, v.HasRev) + setOrDelete(protocolRevShare, l, v.RevSharePct, v.HasRev && v.Fees30d > 0) + + setOrDelete(protocolMcap, l, v.Mcap, v.HasToken && v.Mcap > 0) + setOrDelete(protocolFDV, l, v.FDV, v.HasToken && v.FDV > 0) + setOrDelete(protocolFloat, l, v.FloatPct, v.HasToken && v.FloatPct > 0) + + setOrDelete(protocolPF, l, v.PF, v.HasPF) + setOrDelete(protocolPFfdv, l, v.PFfdv, v.HasPFfdv) + setOrDelete(protocolPS, l, v.PS, v.HasPS) + setOrDelete(protocolPE, l, v.PE, v.HasPE) + + setOrDelete(protocolOI, l, oiUSD, oiUSD > 0) + feesToOI := 0.0 + if oiUSD > 0 { + feesToOI = v.AnnualFees / oiUSD } + setOrDelete(protocolFeesToOI, l, feesToOI, feesToOI > 0) } diff --git a/harnesses/perp-token-metrics/cmd/script/metrics.go b/harnesses/perp-token-metrics/cmd/script/metrics.go index 0986f667f..3cab387c2 100644 --- a/harnesses/perp-token-metrics/cmd/script/metrics.go +++ b/harnesses/perp-token-metrics/cmd/script/metrics.go @@ -7,43 +7,66 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) +var labels = []string{"protocol"} + +func gauge(name, help string) *prometheus.GaugeVec { + return prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: name, Help: help}, labels) +} + var ( - protocolPE = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "perp_protocol_pe_ratio", - Help: "P/E ratio per perp DEX protocol (FDV / annualized 30d avg daily revenue).", - }, []string{"protocol"}) - - protocolFDV = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "perp_protocol_fdv_usd", - Help: "Fully diluted valuation in USD per perp DEX protocol (CoinGecko).", - }, []string{"protocol"}) - - protocolAnnualRev = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "perp_protocol_annual_rev_usd", - Help: "Annualized protocol revenue in USD (DeFiLlama 30d avg x 365).", - }, []string{"protocol"}) - - protocolRev24h = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "perp_protocol_rev_24h_usd", - Help: "24h protocol revenue in USD (DeFiLlama dailyRevenue).", - }, []string{"protocol"}) - - protocolHealth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "perp_protocol_health", - Help: "1 if this protocol returned data in the last poll.", - }, []string{"protocol"}) + // Bench 234 gauges (names unchanged so the live spec keeps resolving). + protocolPE = gauge("perp_protocol_pe_ratio", "P/E ratio per perp DEX protocol (FDV / annualized 30d revenue).") + protocolFDV = gauge("perp_protocol_fdv_usd", "Fully diluted valuation in USD (CoinGecko; market cap when FDV is unknown).") + protocolAnnualRev = gauge("perp_protocol_annual_rev_usd", "Annualized protocol revenue in USD (DeFiLlama dailyRevenue, trailing 30d x 365/30).") + protocolRev24h = gauge("perp_protocol_rev_24h_usd", "24h protocol revenue in USD (DeFiLlama dailyRevenue).") + protocolHealth = gauge("perp_protocol_health", "1 if this protocol returned fee data in the last poll.") + + // Valuation gauges (P/F page). + protocolFees24h = gauge("perp_protocol_fees_24h_usd", "24h total fees paid by users in USD (DeFiLlama dailyFees, token scope).") + protocolFees30d = gauge("perp_protocol_fees_30d_usd", "Trailing 30d fees in USD (DeFiLlama dailyFees, token scope).") + protocolFeesPrev30d = gauge("perp_protocol_fees_prev_30d_usd", "Fees in USD for days 31-60 back, for month-over-month trend.") + protocolFees1y = gauge("perp_protocol_fees_1y_usd", "Trailing 365d fees in USD.") + protocolAnnualFees = gauge("perp_protocol_annual_fees_usd", "Annualized fees in USD (trailing 30d x 365/30).") + protocolPerpFees30d = gauge("perp_protocol_perp_fees_30d_usd", "Trailing 30d fees of the perps product only, in USD (equals fees_30d for single-product protocols).") + protocolRev30d = gauge("perp_protocol_rev_30d_usd", "Trailing 30d protocol revenue in USD (DeFiLlama dailyRevenue).") + protocolRevShare = gauge("perp_protocol_rev_share_pct", "Share of 30d fees that is protocol revenue, in percent.") + + protocolMcap = gauge("perp_protocol_mcap_usd", "Circulating market cap in USD (CoinGecko).") + protocolFloat = gauge("perp_protocol_float_pct", "Circulating supply as a percent of total supply (CoinGecko).") + + protocolPF = gauge("perp_protocol_pf_ratio", "Price-to-fees: market cap / annualized fees.") + protocolPFfdv = gauge("perp_protocol_pf_fdv_ratio", "FDV-to-fees: fully diluted valuation / annualized fees.") + protocolPS = gauge("perp_protocol_ps_ratio", "Price-to-sales: market cap / annualized protocol revenue.") + + protocolOI = gauge("perp_protocol_oi_usd", "Open interest in USD on the perps product (DeFiLlama open-interest overview).") + protocolFeesToOI = gauge("perp_protocol_fees_to_oi_ratio", "Annualized fees / open interest: how hard the book is worked per dollar of OI.") ) func init() { prometheus.MustRegister( - protocolPE, - protocolFDV, - protocolAnnualRev, - protocolRev24h, - protocolHealth, + protocolPE, protocolFDV, protocolAnnualRev, protocolRev24h, protocolHealth, + protocolFees24h, protocolFees30d, protocolFeesPrev30d, protocolFees1y, protocolAnnualFees, + protocolPerpFees30d, protocolRev30d, protocolRevShare, + protocolMcap, protocolFloat, + protocolPF, protocolPFfdv, protocolPS, + protocolOI, protocolFeesToOI, ) } +func labelsFor(slug string) prometheus.Labels { + return prometheus.Labels{"protocol": slug} +} + +// setOrDelete publishes v when defined, otherwise removes the series so a +// stale value from an earlier poll cannot outlive the data that produced it. +func setOrDelete(g *prometheus.GaugeVec, l prometheus.Labels, v float64, defined bool) { + if defined { + g.With(l).Set(v) + return + } + g.Delete(l) +} + func startMetricsServer(addr string) error { mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) diff --git a/harnesses/perp-token-metrics/cmd/script/valuation.go b/harnesses/perp-token-metrics/cmd/script/valuation.go new file mode 100644 index 000000000..9c3739ea8 --- /dev/null +++ b/harnesses/perp-token-metrics/cmd/script/valuation.go @@ -0,0 +1,77 @@ +package main + +// valuation is the computed row for one protocol. Has* flags say which +// ratios are defined; the caller deletes the gauge when a flag is false +// instead of publishing zero, so "no token" and "no data" never read as +// "worth nothing". +type valuation struct { + Fees24h, Fees30d, FeesPrev30d, Fees1y, AnnualFees float64 + Rev24h, Rev30d, AnnualRev, RevSharePct float64 + HasRev bool + + HasToken bool + Mcap, FDV, FloatPct float64 + + PF, PFfdv, PS, PE float64 + HasPF, HasPFfdv, HasPS, HasPE bool +} + +// annualize turns a trailing-30-day sum into a yearly run rate. Zero days +// inside the window count: a live adapter that recorded nothing is a +// real observation, and the same cut is applied to every protocol. +func annualize(sum30d float64) float64 { + return sum30d * 365 / 30 +} + +// computeValuation derives every ratio from one fees series, an optional +// revenue series, and the CoinGecko market row. hasToken=false (no listing) +// publishes fees only. +// +// P/F = market cap / annualized fees (defillama.com/pf convention) +// FDV/F = fully diluted valuation / annualized fees +// P/S = market cap / annualized revenue (revenue = protocol + holders share) +// P/E = FDV / annualized revenue (bench 234, unchanged definition) +func computeValuation(fees, rev llamaSeries, hasRev bool, m cgMarket, hasToken bool) valuation { + v := valuation{ + Fees24h: fees.Total24h, + Fees30d: fees.Sum30d, + FeesPrev30d: fees.SumPrev30, + Fees1y: fees.Sum1y, + AnnualFees: annualize(fees.Sum30d), + HasRev: hasRev, + HasToken: hasToken, + } + if hasRev { + v.Rev24h = rev.Total24h + v.Rev30d = rev.Sum30d + v.AnnualRev = annualize(rev.Sum30d) + if v.Fees30d > 0 { + v.RevSharePct = 100 * v.Rev30d / v.Fees30d + } + } + if !hasToken { + return v + } + v.Mcap = m.Mcap + v.FDV = m.FDV + if m.TotalSupply > 0 && m.Circ > 0 { + v.FloatPct = 100 * m.Circ / m.TotalSupply + } + if v.AnnualFees > 0 { + if v.Mcap > 0 { + v.PF, v.HasPF = v.Mcap/v.AnnualFees, true + } + if v.FDV > 0 { + v.PFfdv, v.HasPFfdv = v.FDV/v.AnnualFees, true + } + } + if hasRev && v.AnnualRev > 0 { + if v.Mcap > 0 { + v.PS, v.HasPS = v.Mcap/v.AnnualRev, true + } + if v.FDV > 0 { + v.PE, v.HasPE = v.FDV/v.AnnualRev, true + } + } + return v +} diff --git a/harnesses/perp-token-metrics/cmd/script/valuation_test.go b/harnesses/perp-token-metrics/cmd/script/valuation_test.go new file mode 100644 index 000000000..f99a013a0 --- /dev/null +++ b/harnesses/perp-token-metrics/cmd/script/valuation_test.go @@ -0,0 +1,226 @@ +package main + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func approx(t *testing.T, name string, got, want float64) { + t.Helper() + if math.Abs(got-want) > 1e-6*math.Max(1, math.Abs(want)) { + t.Fatalf("%s: got %v want %v", name, got, want) + } +} + +// chart builds a DeFiLlama-style totalDataChart of n daily points, latest +// last, where day i (0 = oldest) is worth f(i). +func chart(n int, f func(i int) float64) [][2]json.Number { + out := make([][2]json.Number, 0, n) + for i := 0; i < n; i++ { + t := int64(1_700_000_000 + i*86400) + out = append(out, [2]json.Number{json.Number(fmt.Sprint(t)), json.Number(fmt.Sprintf("%.2f", f(i)))}) + } + return out +} + +func TestWindowsFromChart_Cuts30_60_365(t *testing.T) { + // 400 days of $1/day, except the most recent 30 days at $2/day. + c := chart(400, func(i int) float64 { + if i >= 370 { + return 2 + } + return 1 + }) + s := windowsFromChart(2, c) + approx(t, "Sum30d", s.Sum30d, 60) + approx(t, "SumPrev30", s.SumPrev30, 30) + approx(t, "Sum1y", s.Sum1y, 60+335) + approx(t, "Avg30d", s.Avg30d, 2) + if s.Days30 != 30 { + t.Fatalf("Days30: got %d want 30", s.Days30) + } +} + +func TestWindowsFromChart_UnsortedAndShort(t *testing.T) { + c := chart(10, func(i int) float64 { return float64(i + 1) }) // 1..10 + // Shuffle: move the latest point to the front. + c = append(c[9:], c[:9]...) + s := windowsFromChart(10, c) + approx(t, "Sum30d", s.Sum30d, 55) + approx(t, "SumPrev30", s.SumPrev30, 0) + approx(t, "Sum1y", s.Sum1y, 55) +} + +func TestWindowsFromChart_ZeroDaysCountInSumNotAvg(t *testing.T) { + c := chart(30, func(i int) float64 { + if i%2 == 0 { + return 0 + } + return 10 + }) + s := windowsFromChart(0, c) + approx(t, "Sum30d", s.Sum30d, 150) + if s.Days30 != 15 { + t.Fatalf("Days30: got %d want 15", s.Days30) + } + approx(t, "Avg30d", s.Avg30d, 10) + // Annualization is on the sum, so the zero days lower the run rate. + approx(t, "annualize", annualize(s.Sum30d), 150*365/30) +} + +func TestComputeValuation_AllRatios(t *testing.T) { + fees := llamaSeries{Total24h: 20_000, Sum30d: 600_000} + rev := llamaSeries{Total24h: 15_000, Sum30d: 450_000} + m := cgMarket{Mcap: 10_000_000, FDV: 12_000_000, Circ: 25, TotalSupply: 30} + v := computeValuation(fees, rev, true, m, true) + + annualFees := 600_000.0 * 365 / 30 + annualRev := 450_000.0 * 365 / 30 + approx(t, "AnnualFees", v.AnnualFees, annualFees) + approx(t, "AnnualRev", v.AnnualRev, annualRev) + approx(t, "RevSharePct", v.RevSharePct, 75) + approx(t, "FloatPct", v.FloatPct, 100*25.0/30) + approx(t, "PF", v.PF, 10_000_000/annualFees) + approx(t, "PFfdv", v.PFfdv, 12_000_000/annualFees) + approx(t, "PS", v.PS, 10_000_000/annualRev) + approx(t, "PE", v.PE, 12_000_000/annualRev) + for name, ok := range map[string]bool{"HasPF": v.HasPF, "HasPFfdv": v.HasPFfdv, "HasPS": v.HasPS, "HasPE": v.HasPE, "HasRev": v.HasRev} { + if !ok { + t.Fatalf("%s should be true", name) + } + } +} + +func TestComputeValuation_TokenlessPublishesFeesOnly(t *testing.T) { + fees := llamaSeries{Total24h: 1, Sum30d: 30} + v := computeValuation(fees, llamaSeries{}, false, cgMarket{}, false) + approx(t, "Fees30d", v.Fees30d, 30) + if v.HasPF || v.HasPFfdv || v.HasPS || v.HasPE || v.HasRev { + t.Fatalf("no ratio may be defined without a token: %+v", v) + } + if v.Mcap != 0 || v.FDV != 0 || v.FloatPct != 0 { + t.Fatalf("market fields must stay zero without a token: %+v", v) + } +} + +func TestComputeValuation_ZeroFeesDefinesNoRatio(t *testing.T) { + v := computeValuation(llamaSeries{}, llamaSeries{}, true, cgMarket{Mcap: 1, FDV: 1}, true) + if v.HasPF || v.HasPFfdv || v.HasPS || v.HasPE { + t.Fatalf("ratios must be undefined when the denominator is zero: %+v", v) + } + if v.RevSharePct != 0 { + t.Fatalf("rev share undefined on zero fees, got %v", v.RevSharePct) + } +} + +func TestComputeValuation_NoRevenueKeepsFeeRatios(t *testing.T) { + fees := llamaSeries{Sum30d: 300} + v := computeValuation(fees, llamaSeries{}, false, cgMarket{Mcap: 3650, FDV: 7300}, true) + approx(t, "PF", v.PF, 1) + approx(t, "PFfdv", v.PFfdv, 2) + if v.HasPS || v.HasPE { + t.Fatalf("P/S and P/E need revenue: %+v", v) + } +} + +// fakeUpstream serves the three upstream shapes the harness reads. +func fakeUpstream(t *testing.T) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + mux.HandleFunc("/summary/fees/", func(w http.ResponseWriter, r *http.Request) { + slug := strings.TrimPrefix(r.URL.Path, "/summary/fees/") + dt := r.URL.Query().Get("dataType") + per := map[string]float64{"dailyFees": 100, "dailyRevenue": 40}[dt] + if slug == "broken" { + http.Error(w, "Not found", 404) + return + } + if slug == "empty" { + _ = json.NewEncoder(w).Encode(map[string]any{"total24h": 0, "totalDataChart": [][2]any{}}) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "total24h": per, + "totalDataChart": chart(90, func(int) float64 { return per }), + }) + }) + mux.HandleFunc("/coins/markets", func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.URL.RawQuery, "ids=") { + http.Error(w, "missing ids", 400) + return + } + fmt.Fprint(w, `[ + {"id":"tok","market_cap":1000000,"fully_diluted_valuation":2000000,"circulating_supply":50,"total_supply":100}, + {"id":"nofdv","market_cap":500000,"fully_diluted_valuation":null,"circulating_supply":null,"total_supply":null} + ]`) + }) + mux.HandleFunc("/overview/open-interest", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"protocols":[{"slug":"tok-perps","total24h":250000},{"slug":"dead","total24h":null}]}`) + }) + return httptest.NewServer(mux) +} + +func TestFetchers_AgainstFakeUpstream(t *testing.T) { + srv := fakeUpstream(t) + defer srv.Close() + oldL, oldC := llamaBase, cgBase + llamaBase, cgBase = srv.URL, srv.URL + defer func() { llamaBase, cgBase = oldL, oldC }() + + fees, ok := fetchLlamaSeries("tok", "dailyFees") + if !ok { + t.Fatal("fees fetch should succeed") + } + approx(t, "fees.Sum30d", fees.Sum30d, 3000) + approx(t, "fees.SumPrev30", fees.SumPrev30, 3000) + approx(t, "fees.Sum1y", fees.Sum1y, 9000) + + if _, ok := fetchLlamaSeries("broken", "dailyFees"); ok { + t.Fatal("404 must report !ok") + } + if _, ok := fetchLlamaSeries("empty", "dailyFees"); ok { + t.Fatal("empty chart must report !ok") + } + + m := fetchCGMarkets([]string{"tok", "nofdv", "missing"}) + if len(m) != 2 { + t.Fatalf("expected 2 market rows, got %d", len(m)) + } + approx(t, "tok.FDV", m["tok"].FDV, 2_000_000) + approx(t, "nofdv.FDV falls back to mcap", m["nofdv"].FDV, 500_000) + if _, present := m["missing"]; present { + t.Fatal("unknown id must be absent") + } + if fetchCGMarkets(nil) == nil { + t.Fatal("empty id list must return an empty map, not nil") + } + + oi := fetchLlamaOpenInterest() + approx(t, "oi tok-perps", oi["tok-perps"], 250_000) + if _, present := oi["dead"]; present { + t.Fatal("null OI must be absent") + } +} + +func TestProtocolTable_NoDuplicateSlugsOrIDs(t *testing.T) { + seen := map[string]bool{} + for _, p := range protocols { + for _, k := range []string{"slug:" + p.slug, "cg:" + p.cgID} { + if strings.HasSuffix(k, ":") { + continue + } + if seen[k] { + t.Fatalf("duplicate %s", k) + } + seen[k] = true + } + if p.llamaSlug == "" { + t.Fatalf("%s: llamaSlug required", p.slug) + } + } +} diff --git a/public/logos/apex.webp b/public/logos/apex.webp new file mode 100644 index 0000000000000000000000000000000000000000..af2bd8c7ebf5cb2a2da924b142e6a6695979cf62 GIT binary patch literal 29302 zcmV()K;OSoNk&F)asU8VMM6+kP&iCsasU7?`9MAZ35WsEhHVU3Lb8(o4^Eoxgoyr6 z0KO02_4St^Kf_@l|0Ed*o9AGk$;kapQdec3$PQDnPcU>79QBEx;ry%lkLnHGw&s30@!;om}!g2 zCaJQ0Ns?jYqq}Fi^X^hrow=`F&KyblNTS4&`;N%Se zUONInNlPue!MccwDHJRB+5&RxNId|WeG#atOv1U>2JTy;yJek_2<(&9JyW?ifF;w; z>M6Zo6RIlPdo4(D=*Cz-uv_C%*#J2;Aj+txlc_F>EZPi*vUQWIUwsD7509FLWP$os_Pj@?cxw+ZF z9ZK-4+O})kwrvF(okph7=p<`u5zT2%bN>JLh^4iFVkAk9Bqzgs`2UQb|Uu=HFQ!?ByV;haw~(1c8VMcqx|f7g2Z0T!IiW5i61Ku-q0RA|Xh= z`fHazoUu*Y6e}r23{UIpqt727&V94}{BHYs1nVBjeh2}+YPbE4b*d>w2$4W7@2?3V zWT^K$f)J58*4E52f5T!`N*;8)gs>SytV|#nzT2_?Y4=j~ti?K$C1W8z7s4zh5C52X8wnnZeQabH!$t@h z&LL!dUD`6<#A30Cx(o*YNiZlve^v8peeGob-`@__*W2ss^tk)3^%ajNLi)~Hq?Cvw z?op}?B4J7h;*gKe5BuKVRUspRNJ+n+`p&+TSd6s@DP?tKBSx5n5Qwa=gY+FkBHm=K z>t_aiYu~4Rw?}L%e~5@!gkqDZ&|keTB7Hj{9;e6daRk{&-|KzHj+2nSTSE2_u^2fg ztB8mb;F0yU6GF(4VW0cDpI1u@A!OqU8DlZV312vzK zl2g|33o?BN`S?7CO-v?}A#*+7`tHp~K0fafD}e|pD=90_h{Zr;;Nu{Kc=-HrdstuR zM!tR+?0DgWCt7F`V+)fIrdXQ{4|`ZVo;=XBVg4%#MUnU_D2gIqqbLep>^&<*Q7Gayad*$F zPvtl58iU%1 zc5b0}CtFLnNwb%rv!%0!rUQdk(V>29+g5E$wyj)SH)ckSB@-C~V5TyI$`}TVg2m26 zlg$}MZB%S+TedCxJnt|5{|`h@eetgPGI)L&dDy_i18bWAcOuMB2* zE%McU(70Yip*V@WI_0%5Hf+ncH3!843R$*nAj783met82jrb!?Cw)3HH^Cj8@c;g99b zy>QVj*c(>EKUGq}^rUUjEG11^bRQVB3=nERG^}>v(6!*EMK2B7Hpr9X!dNzTSJ}fktDAg+PcGMWOz1=7&&wHrZZe{ov{F#haNB60w#}*mw~B?Tn45Y6@@66r3Vr)`?haSEz%p zw3P9tH3mG4z?;%y1|r-$taPWk;gg;zaOQ#01Tfuc39nF%hjgcm;kq&0RH5)xrxm_w z_Y|~4hI2YRgoQ^tz5Z6L0pLB%g7Bm*JY-#I6`qh9%AEpnXQxvc*_-N0gFKh4yQt-1h_b3G)Ab5-$HgvS(maj z$Jfkb=iJEKcG=9U6rMGPB`sGMOd29CI*}h(k;w z1xW?8U z8PYWe&Y13MrrUk%_l)j)N)J!#b`PM0L^vW(BG7A*r-Bg3+s>{t&fM%i&{&N{3xSk1F6@ZNuXmd0<+&StLGL}Im#1Wir58;_ z3AmsfqV18Bjww_^qTildbHqX)R|cILu#m-8k;}*3;L-YJ=43(DszMyqt3s}eC!UVE zYmGso6dGI!DOa^nQ6)dbm<-W%WY;sc`Aw`d(@8{NP+VIv92t=ik-UAB%07<_njc(# z+?Bco7phv%JzN%%GUuOIbpemg9V=5U1Jx#K6f2lDw4qQRt=5d1$lw~)n(+iI)H+NP zUboRL z125LlQa&bOA*j2+$EZoXEY(Vo2pF!&scw^7_kx|iZr%1~j7A*!y6cW)ac|G=@E0sUc22S zoxHdgJm_MrA7Q)wR#Ttbtf@o8C)>q&+v{InU)xvxVXq;WqrW+WjYv}#Fl&3m z#h$pXxOkx`Px{VNK6CL{5yeFjE#S_@HYaz(p0|ly#t0OlEWR7)hX6U6`?GsF88FIuXuWoXgR_Gv`_d^R;aO)m;5DJe>&f(j3~saA_`r?#6 zI_1iqC#vzv`JnFOYKAA*QZgumGU!*WZO>jtetd>t3Z>w)>y&<%J`DA}@CcF!{rEB@ zHi_;Zp($1We7{i($In^RrvLTj@_vqIS;C#h)R>xArV6miU{18__%}*M>ND55*uGOK|9H>`&azqvA_Gj&vyQ0@j2B4lqjOn zOkGhOPA|<5wX(j;Cq6^m18G6WQRQI0Vh)8HPZ=clPy2 zmUYCZ@4ffIfM$19`$SFSYKK6vci1K~QifFXE~Su}nY#tszRjh<3>f;f<<(5`fA1K{ z!hUYzLyVp*sFP)7Z~r|)Q9Q`5tfYiT0;uVupRDP zqDT-(eA>hn6)_4O*TvNE&-!jpWou6{S$eR>SZaN_imcFc)7#J0J}09|!r({u zALRTm*OnecLv7(*s+oF{WQc#-*nS$OvymbL!<*%mPZQ@ghj(d)2W$al!G}hi zvQ=o}J0DHx2a&D1svV1HusmZGx?~zgoKBK+x!c&sm4Tc5hHd&TetLkjL*I@1<+GC1 zpPSpzzj96oykg2*YY2oe*5%>5CE`j|NLa4>5W#<_gP=lg$*k4fx|z@=GNExx8%W!h zj3mWA29rD{zUYZwUE_l~s9GQ-YjFGtr<&%|^ZHazp@&@@Zxc+x@q^;nlt2WGwKCdD zJuH`1xs0n6gG3v^oO8}e2Mi+0bmw>kpdMnQ+E?5YE;#h=%Y9*%LR%2#1 zYtH)z^DHs3wS8+E&`*h#l?u&yiHq~f{^6OX#LCM{J67Ul7uVYfFE%PaLqtg_)zVMr zQ^FNfsfWkp*!_(0b%&hK(gMxVF&{J~fSNP(V7kwBueCk3LjA?f8((I z<^dY9$KG<>s-{!{j-8r%l4bG9oK#&?h?1a6w?t6AQkZ$8dX>^evumRD**>O}mIBRI zX+DHN3$Srcv%_X~tMiPGDuguZHCH+4$@OPKcOW8C@y6NAZM)I-mbVmgZjE)}(!EIO zhY`~u)I*Y>kRaR+%q%MEXT7}c)b<8D4v-QQt$BEMyZ83Joj`;xITVSSQ1bG6p$JG4 zxvFBR#rTjYK{*)?%gQ6mu&hVc{FiR}7yITd(YuTI>Awa4EtS@46q|J4un_KmyQ;nM z5**9%xX=&$fs^rmmu>p3P0IM%A^)8Beoj)+54vbt!*hYL(&-{eK4P0=VxheM{tx)r zx7bNDaN6qCJiFK3XS?>Eot!*tXV8&y|62jWHTL!#@2BEQQrW8Se-&=$Std)*ehGRR z2o~5RnmTrhg!D{pYv12&Lpu}YF)=}6Fl#U|$%ZF|N>#<6auP*RsEVdvqP8l^&B=C? z2C#2hhsAwg&86+exm8Jn+*2qNxVyW9SUg>zOiWE(Jf?|W*|~rlhWn_+CY=KnD9!Nm zd6ukd_{gqBg`z(7LXkk6R7VnIbMkB?N?y1{GDa&;52-NyfYn=|){PjM3eQTfTv^}_ zA_I>dA5>+5H43QMz2G?TUSyvrh-nq$03%hd^5b;lCGYt(RidaNGDk%{d)80`FC3TS zf()N=@Lz(4!P?(vaRpJZQ|z`3<8UPUVa>tx2Y?%|cqJt}^`kX`&ZKQeCB1$JhQzBW zt9|98y>4KeGpVFrzARoM0veoDN5bahQ=z1{yj{zYlCGUuxE4^jS=Q!0KUHdKF;C1E z(s-m306(vU2g55Dbz zi*Z`2LIDcgx$jwy@dwbRLZQ&lP=7VL)v=)fQR14&+58?!Kvm65cf z9QG;COq|c8P$<)RCGg6YGrbjy#Xx8!A)Rgm&d|A@DAcZpP>Yc^V7r)l*7f)Itv`?* zPSWaF9P5+J<~g|m9yYm(`)DJEiRiDZ7lzXfI*kHJXzm9?Q3rpDh$y7&KOt1U|FsFb zmca{RrcAD?7GfcKXMW^LWa}z*K?!S|p7Kuv**ZJp=$y^$rQU^Vtm;l)yBDJ{*EaHy zB!u_AmL6xRcH6ZHRx!DQG8i%V=D6N)vp3R#0ajRVzkdYBuyZDWz8^e1D8ES>I`tTH zj!=jTY7bu+tTw4K^lacPrk0|$md-fXmK;b!=A9{KLzE`oz4G#$bMOB==^mbT&t88- zP=q8cfJ)e19X59iycl<2X04vvEE={&s_lB=8r(9}!Nu3VvmB4hagEV$|8o0fXS1F) zgL3FH9aua0&L*9W>vX~yqM$@{KVT9y!4+FI*qW&wCSDX>n7mrG z#&cg7?Ru`Fqp(}0(xrImB|q0q|9i?!OJWLSEdeSybHH@_>blp_?Jp1}G~s5wbfq_3 zXvQ=0Cd_Q(e9`Egi6Y8UU9fgF;wqZ1B~pYSydT7{3{7V-E=l=H4gXq7Hv3+)iFD?JEGb%l*q5pPPmQ*K0C(_9>ulF|A)wq_DMV4j#yjWV2^sFAULPJLW;JB$N1Q)1D z{eZ8UUK#2C_-yyOwc?&%@Z#~^EcwCOhWy!(^LWRJMo9CtKA%aLp-oG|oYlu4tZtb&QJm?~wUUCQir6?>f?O5SJt0?dy z2AdY_Y15g@#mU)|)q*!?v~cGlYdYB~iRRJeF3XY*cw!eRm1>--<%!|y9hZR{JKmx4 z>_DFM8`oznjW3E2!<)hbPPjNP=r@E)RZ3!B6&A;=6RP(LZsFDF^Sh*I_~@gLn!H^{ z4JB0#xe_Z;=c!7$Rzp5j9caGlbPH!icBtkydu?A1!_yMt>!(bIv6h?ZVpuEMrs~>4 zGX87;-mX-yrfleYS3zZ9IWBDV#*TM7pqH5!?R76JdTu)}aP$+F-xLur$n6*TOG zVe;cZ8^Io}X$yP$ly$xT?t?S{UWqq8_;F2Qc3-RtZs-X)j}@s@Z+kV>SbvT(d|aH& zn0^NZeKY4Cy+$ux=`_B6!%Wb?AvoGv-nFRQc{?Dzc&1eZMn5t2QrSDfHh61V-LYE~_i0so=t3+Z&^Ts;RTGJT$pz zy=-TuWCoV6_n&%0r~C`%|2r;* zOM;ip;C%BbpCS+vmZjv4Zas@5ZR_B8GxD7_{H6-e4C}R*)Q~T;9xL>I$D836wtNxNg1f@^nGg@Zc%Mj0(Z(S)n2UU?0(M$>NwGmkC6N<~U@ z(Jh_vFJXPtMUfDg8Mwagjxyt{+NBacnWUu91$15g7t6BMWjTIeS&qv{hj`!ctN;ov zt+qG_6s1Pq-q-R)(1hqW;%9Qy>kr*)uNF}zo+Lq`#~FiQ<+Y-r(J0nlw5c@%1qxTT ze3suIw^UPsI+`dan{?JogqS?0M-ziC#tH$N2F${OUI_xe z*oiwl69cjTMhsg}b6voxMGBkA4^Z%=s_$}sZ+LA`kD3i&xFG3#{D312EB{kUK2;M=ut`Wj)EZgZ^lmMWM{UnC-PECuicd2c>PL7^9iSm(mB}VF^Qw z;T5B=B*w3eSA}i|Zk)}K5k=5Kl3ZM!%>veIeV^y%;T9d#;TSU+NF|dwMh6dz7+nmO z98FwlhSP{Cq9IgnX&oR?>OPBa0>aZM>S&3%d0Iy6`dSbXW$-4F5Ia^gd@bsW)gwqv}Q6FAL)V~ zs#_Ba8MuMXB4paZ@C(@P%tQ7G_s1>0S{_K1R@k&>0+*_Tmx)DV5U;Y^7&Ujh5F+2lB6uG80 zC(}+HgP$Doh)IKOsElfyxjqw>8rbhSgJ~s|yxclczhjWom2lUk z#=E=(;vfctj^YcRJiakHkZpDVB?TLm$be9FoyXn^tn|2Lx73Th5iUtd3}!e#-2Q*I zQ_EOb>4xEXKvZQ?DoBbw&{9`d!jc|Jy*tC<>HMREnY(qVHlUeWg#vZo|4^@(XXCa1 zN~x(GxUkF1OUnre4I$y5_}1NDBH;Y~VgVi6dnC|O(5?$U=oDm!Nfuk#o)6shvAXTU7wj zFS&}O;rH}6R`+?hpiCR99I?(Vyx~>v%aqB*xycG6GYV7sqK)|dST z=_y)ICq8R>PU;taXHrZX?M{Iib{n$6N9~n86rk~IE(m9XvDvSI1#0@x!cGUXED61t z`|QcpSi;IOmOH~NA?v+!#9+~e@ zgX>#h&diS1S_?~2e1TkAYn9DYBU}1TAhd_}^PP<3SqoGs;t;FXS9*+2|(w;#Yn%=JA<+Z`@fU(@z8*t_~h67E)40jYe@TH_8 z#K;zQ?UGLdBvZP~C>=LF2eA(v3TkL{_E2`LcFSJB+_5?(aBQ*^C2zp-)=kXWCHUd@xqA6WwT{ojJ zDqC1b%HXkO<_j*|ioo*`@B)ycb07$`e+tiq*~MGrJUmwS1FlJ??MNUUW%3=)4c?u3 zK6351o_nla6{FU8xz%dPaJj`UE)7zJkVBVRIku|0>`}a|Mjag|$`^uahdt_X;z=E} z2|KLOo|B2e)pGxYWPrjn-H&>0Mz9auZM=k;QIEqndxU%BnAuYlZoRs?0pq@ z*$e%r=5D%cNB__&y_i{Dfzs{V(2``Enp`P^4eMsFpZMv5N=dT^Hx<-A=;!@14K;)bB-nY~xYu+*iHB#s86RDCk#hb1JEq^U(O)Q({)@PiPZxb? zBPsoLetQc4<7r)^P@5b-nYytmC(P3XTKkIOGPz0R_4V^PKYuiZuLtfOS6xJNf?~Qp zw1}oYHFPZ3>(WYpCBi(zrOB@-t5^(1!9f~(EovzripSpl^TWw#b+gq^br@8~ckF>g0B^+n`+iQN3V90xC&Rs}-lOKhJSn2znpL^@5H(g! zwjizSPgmdko$^Cy5}W=xKfLOD$D=zB2C9JKb!pu&IOieG27VM7s8khuR=L|u;hPx| zPi|*++%{!Yc!Z%n7;@$tj&En%qN;i+KJv~94`$R@ojczfUN<@4yH%5$4jrj%*Ju7j z5HuON`eK*6mI75y7AgO^~-LLekzpg zJdY012Ik_-V0WgGm>8>@p2j+ZcgD3W^8)e-hxoDsoHWiJ?$S0MhzH6eU#e}6ZEA3< z1FZ?IXpL40e~#{`Fjay-h+h7*6|^GjGc`-Rrj)1;}tP)}NJna&2lwzv&@R zRO_hTa79UZZuvIzW44QU1q0j}p=xXQJkOJO@cEPE^A8@QZhT3!$}ZO_3bs0?11&YAJiN92 zD;F^Z2R&O+qia$gN2O8QoW)xZn>#(BFh` znl`RPSQ9d-z$TTGNu#wns9wU)LuxFB_uJrCA^ShNr49c&3(PHP=a6* zg1M=&Rg)VXLW@vlBBnNi$2je@?+4gyQ}QI;%#w?=FchQYHBEn}^mBGK=Y3){F}`1; zOb(o3d6|9O9;buGLO5c1#K+q7sLBgvy*I55ZxtQJw{ZwbF0Ld*u7@fFK2OYk&Myg2 zY)WeOoLvXk6eYoAWwa;gF#6D@kyJ)XMzXhSfp(+Cx%5*UUY(d}5)Sl~(&Z2*p^C-w zT%1EGs3^Av>w$>C^`3%8aBuh4E3R$!9VA3e38#g55=oSTVLXA35xG{zOFy>SA=_=p zBmuFkfvfu>PXLBUx!s?u(}}KLArGI~IL(=PJ~tSpgcg)vVi$q9)>>=Pno+s>JfD}Q z_i1KMy4@IHhSrTyy_}qRKamWT!DXGZ%=^UD5;(`gxeNPxCr6vXtj!@NUF4#Lf-5y| zW-!k~!_gpdMc|rihm?u&q1Zg;JKF@+)hAYRda8@N;jw1msyF!lS&146VrHIsYGTD; zE;Ua^@7mA9u{Dbgbt=1!a+Rn8(=wMVOM?xiQqAzigNS~YeOh_iCS{Ye71vyM4kQFk zi=@Ya)((m=Q>cp<=rB(>TM?rLXO&X1mt;t)cX@duX;x{7EHZA6Q~QB{)#yFjuc&pJeZ>HY~lB z2Xl+?H-8e_oO{CSu`rpC*7A+>`ftLO{z&*c_l|T3Q8R%N{4%QO;k>Ph_uy_ebS9IQg}X^wWfTDBHx5b#4E~vA>VA!=6v1>%-2iJO{Gq?A&Ss2sy)n7=aMjD;TRxfY{FTtU8Gt|0c;~95HZ+6C|^NF)o-H` zh57dLvh7qtQD7n49B*Xz(eu-aE3PJWOZ-6ZCx8Q5K5qaEB7on58|xUQum)?cIAiZj zhr)PGIT*H(9JIR4QS*nHTU8S0RWx;TWf>|(CRM7LoElR0ZEs~Jm?WOt$MD(d84K~+ zMt4cyy`IH zCLS7eg{E+@PY%Lx(EAdpQhpRu7bq3GR+@fvb(IGkNUv3M5F^1Fc4XIy&ZVbner??%*Xbit6(F4;-8QF7SC<( z4gRs-Zne#yfMEj`}OR{d5Pxd zeidK2+K&MuB7fMbkH5<-kAu(^XiuuJvB7Gz5(3Sdj>wYLH&7V<%1|?LnToDR731Cd zXMNM6jrSrP5i}~5n>lSRA%<5JPY2SOMzx>+kwQU9z0;aOW19!aXwN&f*U8UV{mc%M zh>)jG!!DxOu1oQpxfrPBmG^kVbj+V(S3P!j?7-(qMWq zgJ*KSRb^zyN@tuG;h3uIZ++L>?bQxc?+MQ><^Ah}(PCYDuawO-tFCjWUrrJX`%l>L z>r?U8!iJd*rh*FVz-n|3bc?)HZ3TZs6B7ao(9UcQKzQ0 zAcCZS+xg~AYO&7?(GkeyJj+#HM#lOB;_`5~TaI@WUENR2Uf3PgARE z8bCi`tBK9DU0Oy(2-#XO=Aac+Fb{4xgTpOg0FDWMIm&BNqofYxa2tZjJhZ z8YLwFp$8C7z(aPgh_7JB>>}_Ph@>8mL>0Tvx-xopfHa}*HR=MG*|ZwTVfDgjd13qG z=8O)p>x=ihXcfD^BFn;YF_<%OkWxhY!ZDR{xHgyAxiy1*6G%ITT5Fu2Z&pKLo4x}> z#T1OtzVqC2A}xUrm8OE*g*xA8E2QK;6UbphiyN?~JkA_Kld-GLyuf{1Eg-$T631{; z@{TA+xx-*u^|e+KzSR0%hVHG8P=26Xf#cSI29k~XspsKxk=UY}Z+64_xONJPk}1%g zvD`EItVdr!8kk z{M1-)l<7w@#*ZH;Ldund(zJO6J#Of4D-TtsJ;ZUkmp{!*%9jY=xOTBXRD>TtmL{rc zW^z(!T2<1M;0IF4J}so`R#tOfNtM+~X|SQ=yTb(t%VV4&{nid)H|*jYAKvtnQ&La> zI@ROS$@;QVBc;F0>M~KLW*+FFe^uV9sru9qfZTmj+ByQVbv$F5`85cz^ zEMNCaDx6peQcN65-fsR!+9$os4N}4&)#`gT2%k!l=ZLU7D{a3PmBS>f&UMZZft-q_ zRVHDLH8%3MZi{5QU-WB%F7iSbLvS`T?{W)-h%({gaE%2K;^WBBuQ#s?%h6E*=#-o* zFPmK#E>VQ8dUxRAHKk?@JFh$Xsv=Z&l?Pglc%IhU;oaNSifpEw-`EQcE*?I-AhT87 zRnA+uL7gX9!daVo=zPKjzEMQR)*fF zBT}Txy;2o68E=CEbS)G#P^`Jk{Cj@@879uBbAzO1IV;T8zHvLk+YWfiVG24YSfyr%$HL>j)mT!~E znNOv(BT*6~n^ds?^lf11-b}o-09s6li_>ep&iL$b>pRES>4gYpfs$EO+O>%t1{aMO z%+O|1p8GDCJfQz-jM}mZKuQc365aaBJ`i=>K^>{XPk}C){W*JGR^;L_yZA6+a!C`i z$-~}d#1}Z3lWzhHJ`mtT#q5+P)3dRR@V9RMs{FJMx7X#B_e)QA+CR(3vlJBM)24_m_p-xc%IYlm~ z^`^QSsR)Im{ztqI-~4j`R)ZEq@K%%~#JD-1iV7)Lm%I`qfk}_Lu-y@4vw+!cDvv*o zmr7v|?29Xv9r)9~m_~-(^B>^cUm=t1Pm$*BYP%SVJIiZ}0;E8R<1Onz&|&bBYR%M4 zRgPB;xsa7c4>Y#LCv>Z7{DPnqT2s5b-`(NE7!R%?Br*wjuKJO^a*xiuL#Yx;W2qnno=H@7*OVHAH+Un9x56pSwx8DH$ zt@diKblxu!8QQL1mwr=#2)1FlB3%RC#!A0j8h&jsLousLpQ6-L0cKLrFV-!`34&e` zwBAs^y5kG1)*3Pph^DHS*WEt7_&}7CZ~kb|md&BX%^UE?x+~70moG9MmntUitxvLmS1mT(U<6R?$4cjZ?|q{(#inL z*6W7m`vRpD1QI)6>US7_4OOH7F{EY!@>~kZ%?2G~=!dG-3~ukf#w%V)ub>$%eT7Zs z(5q+Nv_Ji^A6#|$fDjLepL*);bV((4Dq|h*Mt4AaHuj(-F7wKG1{TLpRbCvT^zOjh zwe*VCjTVMVeIYRT=Y5$!*i-iswh9^|o^gxk|0>$NAh*u4+4#8O=sbD#E>U9Yq0}`z z6c2&_986WR7>OBq+V=Re#+Y8Am45MxC;){*(Q4r0^CT-=kada<^mn}fJ6Z(>kHp7Z z#|alN*U_5$9}j8HK2UT_axd_GudWPd!_1bxDa*G`fyw=qgNhR1xH(|)RK@=JU+y3G zRsK)on@;)86^pl!cU@N+_E_QIJaJc^Lm>L;9qd=2*woSqGRvVW1!w-}!8?#yF1Omg z+udWUe&hmyMs6Egz8BsIg*$kU%SEqLdd{*ruKAp6P5q$y>ub))U1#iMQjE%#V^Wn3 z!Ob4#hj#aIhc*CoLWg^EbNsQRpMT?r1MAAFZW^sY!8bS{C)vy*_M%JUG_x40Ge;(V z>K}B4CxzQ@rEp&@3oQQi+fPO+h*GGO7(71@5XDM!&Y&I`|0uvvwk z+aGU1K|}3n=J&`#@1D^O2Hkycui87zVTp?+$vRO*3a&Mb9_Bsc(1L%>*oifa+JV7%l6R<_vdjT#xdU zx-Wcy&vxVByJZ4^FdKvYp|?P=qSs9=P1#86)|E6R3Cd{5P?4vkDy-MeRL$D!TlqH> zaB#GN(JO_7gXXrihEN^{gUYvb*=zH_==^lj1(+zw<85cfNKONp^uUmw>35l{Qd=E$ zcC(^`0i8+CbWFMnYb^37gXZ(&&brDFVq=FD1sG$Qu=~A0}-LeCMGOVL) z+*KM4B9cZ_knug>Zg7c$yoaP` zXisj9@YxB>*Tt0x2;V^F`5rJoRB^}0Hzsx>Di-dR<1%IC=B5pFfYd8&G~VNW)@S&f zG%}TfTCo?;>D#1LxoAD9%AMXUFlW9(GaOr;XP0)4{w5Kn>?s|wdZ?sE&dsX$Q{cRz z^#+56b9-$h0v&Y2v#p{7l9J&_<`kMgHNpxl9t4*z=iW!zdLWfK`i>dA^*S&>=<=7T z?Q}VI?S!Hm%~U-n7$p}yHM6-e&5?F zRNW%Z9S*r|(heQW57&D;r5nt=ld&vFx?=7$qLEob@L|D&obcs7%SW1h4=7k?+MNUJ zSC+GNIa@;X&~e9+B_p-!#>il<`f`u`8K3eK0I7h022&}3Me^h?TBbc4;yLDt^S^@3 z=hqbJ4JQ%rD%CPknFfD@7S4hc=}G8&_8KH4iZy4P=(U? z0!x~{isZ-=ovNV`l@5b%jt#I-dza57?z#A|3`V83c8)?2qcPeMi_D+zNdS>k{Y&nz z;=lZ{)^bOCTDzhC)nN)yo1H1-L7YYDpE~zF1z}B5^kPg1?V&+yW2c|y!ezgrBvXs^o zM8C^XL$~@(ks$i7pfV)M(XG9(6L2Ft?Vpl3Z{J};YEkL|A-ARte#%d6WLcK4R4ub{ zjRjf@x|04)j&+gES(k6Nf9+AdM-_(GQ;6HqIGK!TH_4X~IzmPG4h}Yl5;btV3U9kj zBsZ#sq=HIkoJ38b4p1W}Jp;F(sZ*NnFbJ*WeH$6&qg4-2ke_Q`NMduk z^Sy@%$w*SAFN}T&Vxvd86yGti6Jj%NBCMMmi)?D%$)boTsKR$lIhq}_W@ca|5QDhz zK`zQ*QRhR|)WrwJFHuuW=RA9ny>U7LdSC%w6PYZkaKJ5VZ#FB*b%uW5{6xe=Wnzjj zVwCE)g>ds6d(P^leE`1U^%QE`f!j(-X*(p-A%P++^X$UT;pKITTRAJKKp;aU0-6oJ z<1-Yh!|-d3nPH`Eq~80kw5o20`jOt?Lm7Bt$BHB#GmtIa7+eAl1k}=n+(ki7<9t=c zb%Rh3$t|BCrk+7;GZA8u0%a#iRdejZ>;3dUxhu|-1dx4TYeEj(%UHC zh;*?F?-M)|;(>x3I+4KpAT;ds6*ZQ+u{wy=Pl~aU9Z}UqB>IKiiov36(~AeovTVjD zGcpD3;^OqZ+}I9jO=7BEUSBSP8M`{qsNT3DM$Kl~vB145k(>p(w)!|1-FT1wYbXAZ zi*FL}BPh{Mj~Y0rmUqG#Q4(zAS?l6p@CU~VEwLO+DL_&|dr3q*!nY}PhDe(JnH6(F z$*b|lO5wH)HX|35ZEA+=iiwpT*OnXH@XG8+v|A5!!V}h(%kVC(5VaN;P@N9TuoAC=iiGBrT@M6A@1Z*ES zd-a|%kCdSdbpO_`{W=Il$CqgG%iWRR@EO0&YkT0j1GwD}PGrWh<3r`apQlB8n^|F> zyC{rGqhdhp2QP?z4(@2^1QRv`9pC*O6_;fJ5yglA&d#nM7Oh#0?)Mz~=IxoJ{PwF( zx|1&ORL4O2)vtz6^?RD=aL{;|NXq(}PxKSyGa){ImVlJtr{I|>McorNNk@|Q=Nmxh z6JoS$oEWzlzZo-Dh_SjcSTX#;TCOgyMz^TEgi|of#9k1OSaS~nAV4*qc!Un4#nq&fmmIk6B?F0k780nl#|fa7Aui?~=v`NY8Z+6joK5$CuowL+-O`tG7=!7n-No+gr12|^^*2hb9aHM8lUXR@ zWiHb{Z%AA&jMC|-r!E%uh!>1V92GFHF=91?*iod|(WT&Hw>{T3Yr z8`urK96r^dIF@d4LmFcgm;>n{|fpn8Ma4VoOa<&xwL552|Zw@c67SM5&aCGE_7o2nt0>s2I&A+zKOL{#Br>513Qd-lm&qj*G z@TGnzQBrUQq0RXnBiC_pd3sA5YUH>l`I0Z^rC;f>FT9CQYk4dA{BtO&vabflB5?<{?Ocf>pU>E*t;qMd?|S#v|7!7OM_)qusHky<(){WM64 z(r?VF;`9yfeN#Sp{Bvb!DMIw)l@*9v{L}e1Gnu41h*PUh$E_XC_vd@py&@rT@ivzf zqF*6ZFQYp}AoAxZ-q>}BeP&-vzQ*jhfk^f(?^e7>>MfU)qo=oXgu&hjqsoqZp&PmE z?v~Lvk+WWZd~^O#P1i9%mWDU!X@E6E)$q2u!n0=`CK}+Se%+hlv2)l=8Z66Fa4sw{YSuF;8mpgl^?`Gn2FMroYL_k<0^kmnFH*O<+aytK945xz{_}eTDGd7a<8l*H5J@ zT+hHq(LY}euj)CTo4kLjv=xw3Y0=@U%Pi17S)KBUX5Rch52 z^YQ8JK`CoZ&AAr0`UOco`QdAn!Gwy5TASCx-zv#Jy#;>xJM!B;?~nfNnd5aztP^2d zg67*cpj#x>CNH#?mk&SxxUyG))2H2`3Mv^b7S!vfYuOrA^-Mmlu|9PQv=IsSz{0t4M^djPtbA&`J?0q`DOE!G&u(hj`iZV|l}oN0p3DkL8!RA-~BJ`6GUP^3UA- z`8S-L$f~3#dJsNeHmwCAkkkjvbst~B0rh6jre_@z7S#f$J}hNLI@7F``L za@Om8(8!L^rM9VZ`(K=}{cYd$OWhRGatc1Mlb?BaM}0YkO0R5(P8v7VJoZX1^s`x; zYDI#e>w>rzH^cD5^Daw75Alw~NIbS1-bJZ1Y*m~;e%#P8sYto*ZFgkfcB#=bGt5^# zg)qT~h$Ci?50%s-)3LT%9ASk;9m{G75-TCbPAg{_W7fJap?26Wi8%d*VOK-8yK zvbpE}K05iN@+>7-mgU9rV%wXyRsATQA*#@T3Y}Oziva()z8N)WW}b3=tx{I2;YTQ= z73AepX;g$7c7@bdulv@I)giBCf`=rC@YzQkE2d_1xd>7^Y*l~l5a{BuZ{O2>U7Pi= z)uCdXba28#L$d2l8BAu}-hPW&in`EQDUNGLX=4LKSf1K_`T<2Q)T86^2<+!C4w^WhYuq;XR08IlN$r4A;pdtbRO&WSz5@`)c z!i_l$L~lRtRaO%psZ0qCpP`UqYgua273Fl$kEx#DN@=t>lpAB!sa6>VlbCF^W2@SB zd)_4-*4}&dk&(RZ1iTRYq-o3_&rdbKLIh_4UqGBHj@9JpN8};6Mz4V4R&1EWFztOBC6?i(@ z;&a6eQ+Y^Wg~pZ0q?d+g3fi9|xdYU)lplys*>-~vVe)xj z4`M*4ifqX1dRcgHlU^9DWB^T?5_J<3fGe3+xwnaR>|(K0$w|(K<${GU?df(;w`aTk zWLcKAj*dYz@zLAXt`|FWPw^DVl79&P@RpwMBRrJP?(0$(kP^bRo12^Gm|x8CYMt*4 zi)h$Y&bFC@M_y4V(6Cjy77$hh(&_a4%iWN_>B|3rr){jn6Vw?&R@E!?G&`>0ZI17U)CGWO!b)2`>t9H>C$ZT zsmye4<>>Ap12mD7p)OkxN=(-6`CInfFe)hAV6K}1ZR%$9`WZ;XbKKuSlubLfevDG`+lHCL`@s}rmwPHCN zOZc|u#aWqcl3B*Ks5HpEr?j7lz;QV)$K^;>lLpe4&BP9w&>%vrH?fro4l!-tHlHn2 z6D}W7gy>h~s>ARfF98wdMKdXyu7C813-CyCZglN1cy;M! zW-K>cO?lytds7yy;74TZcYoypXQo~=H)9hmTcPWg_vX*$$TOTMljlM)eAEP7mjo4( zpoSWESD(ol!t%gf7GKH6yi@B3r4^NX#z+l)J8Dxv4){Beu=?bF_PsL4EK5*vgP zqZapr>^^w)EcP|x(A!>DX>D4Pgx`A(bz~4NY z*0?PST>-U~`$hQ*YfKGX9u{a!vl%8gGcyk2lLHipej>7}kd`8m0R!eBXW6*ymbaVY zWq=N%TY28VX$IE@s@E6Zx>t+2Ee#h%>O55LwMa28$-@Uk`)%+3lu!3r8g%NfJJ{=2 z8GPmPQog=^pbB5VZ8FK-?S;Fo(&2xQmNaIR3mV`14W3o4Q+u3s&0>?0iHm2O@?MPt z6iNh@P$x!7*DGp^kO->hfVa7cXZ|(XEASxrXMEAeZOdHE==qwbJX&FGD=cyn2sq8= zr8|4AJw4wv`Wm|0{;kZga05Zuz$-r;bHx6>jU>b-CsFKk zhTyi=0IZ{f+#r5S%kW*gd67!OrnMWS-!B$>R^vA!z+_;-zM6eM+c5c%?aB{ zhvlq@L=c3V*KZ#m6BA3}(MAO(S&QmOvJVVhZ| z0l)@m;YZEO{9KRU(Jz|_lBBys8}5%v8lotw>&|)W3lqiM}xPMj33_dLDko%i(b2x=!u9CS`jQwIJ zLQiIJt?&sUHG-5%XCE2VK;lTn3Ph2ZA%k@R5l&rro-+nkKkO3=vQGbKIZKkED!i&+ z^GHlA6KyGuOC>$m`x09@m_&XUDTD=G68?3VirbbJqRsr+iQNhgETfzR)Bbo5_6K74 ztQhHleUYX9z`AF3i5^RsMqzj4_?Ur!0IV~$64cWdo`+ZRU4G_uRI|6{lyl;rJagjQ zZjW)V$9t%5GK?KZ5r$m%JL`<%gmmMj(1V3sgaz9|3s}(Lk)4{gE%r3#Y-3a_()M9S{g zMOwBIr8VEHZ>j?!us^)EZcr<5_&Z8cUK>GU)~}VKBOTvD;!7s`#lQ~r__X$QBEp#7*uvB( z4Yd9K@Aso#n4aC;Qgj%C0GvlB0!~ln)b}NDJ$jk~i;%Wu(=m7V>cGtdh*Lq2<5L@|~pbytEJqJwI(EP0&sDOob_GLZ|>UPcu(E!_H|x;im#wG+yRT zglj^4Ci0CnckUcdqXdR#q9_qxYV>12;9?w!1vrKFcCl}<9FY#k7q;3N=-|?5*|Dmg zi^WKtn7L9vrJf8Uc6JQq*&bzuwE&V>>-il78X}B>DqLMkJu@5Fk*;&(D4l3m1O29L z>D|5lvk7UPV=i(lpM@_QZeI6^IDI z(v{E_%1hJzuEWW>S;qkn{3-cL6_2VX@kd=IXQBWtD8bR%S5wEqQdj>>gIB3Vu#69%w5Bpl2ZJBYQ0Hw${mR&f3L#yi)+?`97s=DPh5I+1x+&p zxI2SQ(;=KFx#jG=dC%d>FC2ZrG?bYrF>F7tp3-*$^I`L9wpeVq$J}e!m&0coq3PF) zPML9LrG657r87Gq)1k^JHV+%%}d#5 z>hc_#mo+2Ce&I!EI^#E@x{Qqnc+$&=JzO{jv#w=n6Xi3mo54Fy1QJv)aYZ74 z8GOFDTfUzc2N6NXMrcV56tyuv!l4gTo<(EBAuxdPnZ_N~H&(Dsc+rh#YuCvLbmlr_ z3)wJiLN5?Adz%9{l7K8kGDWP2_4_qYR7wDyzEt! zsm}K@<}Gh`Dm~T1iIFXILus3WMDdi;ixybS7tgBf}PiR2>tBKV=*@ z+ZaF_{Dv|hRToeqP+K&_YRAGLwC@WR{L(*7bQzIlsEb#W&8ZX8Q&CYvLAP|xtltWv zLF+x)863W_lK~YcPvnFKJ}1MSVhmc5sDd5ZVrh}Y#!lqq z_y)#87CS?H`fc}8nV&$rP8jJFRs@T&VDykuja(4-DF)+=kM2Af0V2Q$WAK+SzuTj4 zRV<7K$uJn$ra66+eP1xDMUy1UCI-7Wr@mownbR8`PI_EeUUY`Vj%ON+)}9%2V}?< z+JM|(oSG&o0k(9+n;LiJb*~fX6m>*bi>-DHP`GJ&uNM&Ywu;{qAI+M7F+k%Z z5a9{pg!)GI|40I?LD+S@^!T|fEq3Xd@)NIrEUc)J4#+4Q6M9|ex6#hL`ysm{iO=dZ z8QHqrnTiN2m(6IIUIrEDpkSQwc?SOiVx-I)x^5HWF0UL@YJvDl5;C|))?1YS6FY+i z%OD3s!D8wS@VwA>_`mO}-72C$VdEN{Z>DWt$Gq~MDRUcg9V#HnSd){g@0cg^TKI+_){!~~j_fLPnl z9KAW(9^f0xR0d@;+Ui;-7MdC9EOVm$G2cNYvRKXrTCe$)9yFB;9Gxil=lX;t!R;yX zIZc28r@#b2Ud%o=&%OS%=qt-ZQn15+`{|cIF(Ehblq}6aO7`5+b@to3KH$z1i-?}& z#gXH2G5H2EpVv*2{v8$Z&a2wSw%;?wUrb`9m@Ie0|Jy8liZ-h z!rm|XC2q7!G8@#^h|}FQ%<^%^^GAV*S%Evc&i+rIGO zDK^4!${V;OP&viq|9B9&T1a`kREwxhx{}S`vxze}JL{wyh|s_Ncy-BaIGHw^&98^- zY!K-_%DcVFn^J3`y8Xvr6ruuIPlltQRjHgHCnulo8xlF2mouE<$2l{(t1$x&q==Uc z8PKM%qV7bTZNwY>dH|@z*xt{?lI$CjE`}nusG(^2*5}V#2~~tZ5t3TJZM3LqlZ(;? z>$3gao@RndbU4o1zMe%qJe`OkR6?AD;^@=O`c~@^W^S!twVHT`4uf~If(VRB z1wlBx)hiXF2gjNUYp`#MfX5_(+A{!A0OuU0KJkKD3l1d`IQyiry z(z_SlhOc(Jty4&1-Y{CD;uPPzJq)slO-PsIbkeHRyf&X=QTmY*%Unq^T+gkknb3{4 z!rg>53FEYO?IsgB%G2HR<79qq2br0Y!^8Sgv<49m=jF_*S!*m%hUYsBP~|!h%nNC` zJENUey@8{|fpL#z5tpFekfp~jF~-bP$Ph{QeVY{)fol0O-?&T@5fRZ}$Z3RY+Hadj zGO4ju?#vTz%+{aJzg!&@@RK(UdhqA;K&yE@PaKs)c@(EYkznp7H|wKAOdS}%gFxox z%O;!jQYAIqw^HsCUTcF

h9mb8aR->79v@#~QteWrm!&PAHqIT&8Fc=^jYyM2Rb* za2gaHD9Xfh^K5dJ{b9ME>Ym-l%2C9d*rnG^WN3@kg~zWhCs(2p$kL5XW^U_xwo{FC z4X9$~q>LCd&wk!o2c^$nb9?Dqh%T{Bm-FU$@{``lpFR&Zn;zrnGVLoYr2dnz1(F9R z_)siHw?AY_ba2utL%&U-tVRnB^CWa5Ke~%+V3^o0?ST~4+&P6bo|ejUx^j1CL9Mk` znOgddd3HBH?*T7cY;+#Je&iDQddRAhc>PQ6ew{ds_L_#;ly#C1{Qc>iq-BYCoQ})T zlgnWfB+y_wV5%>c1s!>|N!mP{A+hTn&*yn1l$uhqrKsc{4lw89;p+*OFvv7ZOFUrL zN{(Yy)Cis%yqmWagAlk!VXo0nb&nFpI)55xjICNoxMJ@qS(CsI<$Zx;xX~O-ucKt1T*MFlp@`k(yx%jc$r#3#Z_=1QRHa% zZ5qG8EbEDtgxo*KWKytT)%)V3yD=*_4N9>ZsfNZMIJYq#dc-1A$l)7C?_SFHlH)IX zTNW0J(eqNmg!r=J)qB5EZZMnG?lzf8*qGYtX5tJ(#Q+03RUw?5`GW6jbVQk42Im1i zPB&FpMIT^xtaeDDlq|`FB7!^}S83n~i@MuR?w(Ir(eziV)miUwIDlcmXkf5T#~uQ) zF(Me&)4Qkp6aMo1|MbLf^6Xlg@#bOo(!DCYDvjri7_D7Pn?drzrtx9`Gggag1d4lC zpJ*j1sa@`Dr-q{+Q6#gYtV;>N2a<>hpSW>GT!3;%ttZ_H33Q`TSgkNSG%!|RFd7`` zL8_QVkR+^LMWMO%&rJS|?4P;&$NsUu-*=Dywy*e=@7>k(d8EoFE7HiBGHG&qfOGWl}%dGSNoAq#WA6_T% zKPDWQ`Zy*XUYyxKcz<#BvV#A=iMnW+Krn1LZKm6d3PuDu!#uAmE}LFVEi2{D1d-RT zZ(`Rp?|+v+{uASIA0EO@_rDY5bi`%zvMjUy9b#O`fa)K7yN1f-K-M_sRdJ=ng(~Z} zI#PqAymrqY!qp%~u~(@w`-d!gxw1Y#_NOk8Ol#Yw{_!LcjNGXn;k-Hh=XbMFZ8!s6 zXhrF}{Q0l=TmNglH~(?r5XRCRV}r;Q*H@1Qk32UmrvoZ04*E49g$(Q!FE~>XQLN{f z4ZQQRLEKIE=56mk{lq@K`Sb?JfjH4=Q+4X6ZRNlsV9G$rdf5zTm%=<7tpweE*!-pa z{h#0O^tY+|@wjy6a9nhu$=Q_#UYo(JaHo#&>`LQ=SnZhIIG1W^W?J2x*Z7X;r* za@dZZkq0D`U*$~sHVpw_w*gKNi0*hw_g+O0t4gN_0~tD^@a&6*%dRI~%%+~PfK{$( zntyV(e>wKl{XpG9XmJ9ND#o9?qmz*t5Y09vyO!aXRcWVE*3S8U)GIc)$~%k$BP)y?FWZ(@$>-I~tzw z=r>12Ne(Na7;I$MMh$@7ogf=@C+K2MuBOCl1LYx)KXLkZ%%+!YLSE(DXUEr$N9w*g zE($zjAnNV>9Y~_YnfvDdANo!pN;$vRY}=oQD;mz<<5rT`y|vsAoLRNi0MR?|dYe;b?WY>9W|z{oe2&q%|S2=wtT@n!2`ZS&dJ+t-(e4{>6S zTQ(DwHfQ?r$8?-MT;_S6uapa;HIzt8H?1`vw<)WuFJE;>U-@O+`%CAR(dK@CZLl-( z6k#qbPq%9!O`&M81!&!BwgQC>$!0(GvO!FYA`>S55PWg%hLy8e&Y+T_GS==~gjsoG z8`e!aR4K*GI_L}M<;gEScYf*I$GbysjeAJVpNi=U5!F6fxvbsMF-l`)rEoc6kb}XH zUXH#<5mY7xtHDBymcRgvUV_UvpvSD-N9H{+Z zrh5N+VqOhz0Aj}%iXxQBg-c|u0ZH_~SiXf|baX5gHYa6bO**#5jLHBUIEq)J9%J8# z#9L%~ed`eHTvSZy)jVx05i-oy-un**7*`pIWnC@4MJ1@92Dn7QI^czVP9~p*h;^u_ zEG5sH@iO`7U|peCqL8FfyaD?#c%2NHds*s zk9Wk}VJ?LaD8yT1`MTn3N-BWf?D|oBkIPG562pyQVD{+UTs=B`1T!ThW7A8D!pZFU z&1SLHGcF!VpyQ|@D&~9hGZSmthLqXrIwS@a*uc;Rh9e;K7Jhu+7W!RT4$e|31y@AK zRJ1svVB57>lKnYVojLNsT*fOxiYIXIs$sLiW)Nt<7nTU8Gd<==e~cL zz=S7|=-13(bzA!db#2q+M3tigX;=HR!n4yjnT@#+&Nwm=WzRy#g16>1JNry0CWZHo zZ+)D0RC}!cX(-;7b??XN&_?x-+J~9^TSH3X2M?1|q~@2|_J6;`x{! z&lW1m#QDLRfvE1VF9k~+N$_!SZ|bFID3c4b;V=XUXN10+e91Ns8H#={$G5neoG2+K zuB8neJ;1~9t~=1xrPYBLUgmXS>599dN)nzmFH$jeGfQnYHOwp*I37Erf2O-W)x9sN zop3j%MllsvIUR3u;=OrSCxbpk!{YeXMs-n2Ku9-(d7VBU=hDR>eA+8p)xWdzC8LEET}iiS<8s(~7ctuD)z#PBH?CBkJ*T}85)8D=X~RVn-C zbhWjft64jc5c#G=xmT4<7Ch|G<3y*L!`8W?RGQcN%%Hakw%) z)4bbM87v7wT^2E1K=*}|4eI~mWDKk$@Hz!DI3=xmxy@4+DW7zNLTpL5Dviw@egD2- zBT2(#e&T->uaBjyo#xfn^okO+C2Z`;=?}9eYd^5*A@?Fs3i->he-YFELyq1Eck3Hb6R&PDCM-t_y<|I~M)!kV7gC zX#0Eb?|VPEpXvFMN=-SLtm?LIAv8$1%gFjAJ7NsGE>(e2LeF<3rZ2PZ?4=HlA_VSaLb4N<0+w07_*TQ(yH5Yk7q6h?^zz;uzBcI53xBIL&wp(`S?qv5}mhI-+&Xp*6j2%uV z*)ZTBKt}8&Rpg6Zy(=vtd9a@}KmMxQU*3ne90jnni2!d>+TAmZzLf*tt>?Ty@8kN& z78jF#qi4~K#$0b!Vg)@^X;%PT$v)f;TAOdrd-28_e|dq6 z$C2%r%lpaS>DfEqnSLMD6Lq}A+)KpiB{zumJFfysz)Tcon%XLM1~blJjK;+qpXr%;n}TWM-7_z{PhZ@3PCA;d8OUE9XFeMKc4nIU6>rfdIk~! z11LPHLLuX>9q{gLT-eR!J|C;NOLG8mgs0^ZQD|u9d>NGmHG1CYGyMu4YsK)*9t4fJ zdG=_qX{jB{-r`esnCe_GqA-b$P?CtcrO*?C!YrFZsl15OyS;gU2V=3FvEP;mC5UbP zp4oc+UP~;861%d`Sn&)MfyaGf%8`U9iJ;VtwQak$->zGiTkmjsftJLhSOk9f-I3x+ z-)AADzHDgB_tQ67DG|n4!!f8H|X+MvU*pK*)DXlZfJ;@g=%p3xH%Bdv9UijXY(?#x`~^mB6^&yj|ux z*#d#H(#?PvEjA~mUkSpH41GI&?uYT2Pv<+I^ZwbJ9UTjHBtYS`0g{N&P^iCK@&+c6 z4jFotgBAu>I!K}4(!i^XX}h>`II;H7F`-e4f^4XbQpHCcPax3Zlz3<*yt?&>ET&wM zDs?%q@v^Vmy#1SQd=vXSG{pd3^RE(h#ZZ{n8QT0W{AT7EX^__Fm_93_<4Rs7Ke^Gm p)xySP3YMt!A(Kw}bmq``z0T9!iFecg((W&{p49JB)PV6h5CFv-s1*PJ diff --git a/public/logos/avantis.svg b/public/logos/avantis.svg new file mode 100644 index 000000000..df2c5a2bd --- /dev/null +++ b/public/logos/avantis.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/logos/avantis.webp b/public/logos/avantis.webp deleted file mode 100644 index cc6bbe7c0371d1df951132e88954321f4c8d6345..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1046 zcmV+x1nK)yNk&Ev1ONb6MM6+kP&iBi1ONapkHKRAub`l98%8DY__J*{4nl&&$ZN09 z|NPe>FAxX8oNK zhspWHugW|Ow(-kfm1Pu4XB2E>kX9b?s`Mc+XB@mZ0SfWlbl`YL1L zMR}FMP~>Gs!=~=VXh;-iJsf6n<_w1>Zf87nZ42XJrnJImgB;IYYvQRfa^Su8A?R=}I0GMqQ0D(R5|Tgi)4xP&8eQF=3Qd7!*yG^`J1y zat1|{RT&gURhd!Ys?4JzQ`OF(FjrL>6^)__qast3^{6lxWzMK*6ty!d3Pml93hSaO zqaryqWroF@nqCZxN=??oqEeGNqast2Iiq5xCUZu`)HRtiDl*UdvtFH1k*R6!jEYK4 zg(p97X{DxxQDHkJ*~X~IosuSJP^33c{-e@Dg=3aGB~8wtm^PmOhoprf$IBWOen6_7 zF)=l2{G(Jmbq-a^8&&?nvvw*Sr!=)roxk%a8^w-Mn#$HG^=FU#&8c^29*@U!&fl6- ztMBjD+Ix3u^|s{s`FK2rhI)O;5ByjuwQprg7-BVBdEnPddYcl4SxT=@{I-rQCyca^ zUK9RM#X4cIW%Qcx#~N0{aI07nK2X6nVZb%`gbzKxo-yPSmIO)P6N17geDulnj8Q+o z%^+zvM8$$nJhe3;Dhf1JjQqLv8JaSpqQ9UiBP!e=sZWRsH)xs>6>iX!5fyIGR1p>3 zps6A(UeNR%VUeN9Q&?1J>V~LTAgUlLc8FRL6>g&3qM{o_Wkf}Rs1;FBA<8W(cBsk- z3O7}bsK`)dQBk34MNl|oSx|J7wIV1AWLZ$GkhLNv+;mw`WXM_(6DxFCOnB0@Atom1 z$}v%)%MlU|VI?HGDYK9$5Vj#ARw&C6;YnFXL>QE<5m6v*4u}QPI^to6wlyBy#LeN* z4dPNbydZ8vG-RkN(NH1p6$};fI%1(f-}hL^7=RK9{e=;D1%ew!pd${}gdy0%pckCw z;g%@a45Ki|fKNEl!&wCMnlKKI>(^_-xgM=vzHP$k9?b39Z8hP94_0v9`nx2Y^RX6O zw7r!l&ihacE|>4E=4ZmWAE?i`KJM=OwzXDko}UkBJc_^PgxmkO|8M`_{=fZy`~UX; Q?f={VxBqYd-~Rs(VSK;)C;$Ke diff --git a/public/logos/derive.svg b/public/logos/derive.svg new file mode 100644 index 000000000..57ecd3aa7 --- /dev/null +++ b/public/logos/derive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/logos/derive.webp b/public/logos/derive.webp deleted file mode 100644 index c04facbbe770cd5ea7164e0d948efa488a7499ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37074 zcmV)2K+L~VNk&G>kN^NzMM6+kP&iDzkN^NLkHKRAO-Pa?Ic_A&6etus690cNz;)JR zME@s1Z{F6gdVagZE8li~({I{-AM*Q*EZ^bV924uUwj}MN`2|j*v;7^Pa0I*pqmxM- zKW3vGA(fv(G~gf_>Ph@eNR1O2n|pi0WKU>6M?GqNjAz)pLz_^N-qL=k3%;b*btftWm=t*bc@k(?{+GRlB`0zA!kt8XzCp0 zT0tTt5or`4iB>QPj4!l8=s3OkGpYTcD8lv?+BVKe-IlCS5kRu=2_U2JuZ z-lmfXT{aCkAYKJL!3b+y8$Kr=_j< zDxr~qzY^{|a{A9el9V{$tw~_z$QuAH%&}It{(teZqEi3d`5fIU*DruLTrOn>kI+~@tYAFxF@IOlobM7ImpD=SU!S@qA2RMx;DQD zVLka>xBcE^W7LJZus@B7WTaSGb=bEeWXz=PjfgQ3LWCe73I(CWn3>^ju2}7XRqE| z!~zhp!^MesKDx0#H~!lZODo;VAZxnW^W*}-J`*Bla$&j=w=6v9TaTA|{4HC~9}B+&bAhHg<(Ah;U(_hI%QZDu-m^ z`Lt=o)4H7P>@04N03h{X(fvN~T^+l^J z)uSx6?>9bKWt9`nT}-EOus>G+Th4gu-6vdJpwOK!hX^D|V)$@4xu|0=u!5R5i-cKc zZHn49;Pt;-UrnF>Po;KNEPZvA`yXa;Nwyu|EmH-E-r*g@LRL&O<4^tsa7H?cm36{T zizFJcb3(XXWBl{(Fed$r$?N9lCRrKjxvGet0cce}*R#I=yjY}2iJ}>bjUC^Ps}bY9xCrv<`q?CT;IEjI0FuYYz^4oM)AKxw5_Q%f-B{lAd`V7~8<_UAnL zlpX{~a=|~#KE3YaV8i>epSw3D-*Jk|qHfDnMJa_V*3F;&rUpnwbjE zON`mQgXWT&DQVf4*)M9=|N}Ec)wXTh@{~&*G?^D^vzAu$ zy=)1}jDuRMIYl!x-P*QnZzElF-}eLW`+g{vEi>Cm8H$;i8HU||p=bRQa8#>j%*@PC z5C%gfjulx3(f8^5UU;4xuIp;<9`)*BO1HLuisT5+?++1?RMntvsnr@-GuwxJS2!Tn z(9(tYO8of9=gYi1B#pYeDk~%6|NrY207;P~NfHq?bC1X@8lGi(I`{_WySMudrlz8V z>Z&^C`wwPj#;0ZGyP>MAjBqzo(NMy+4MWQc@5X)CwyoN>BwMA_KIQ^rCL!$4K2b>% z%oH#M7&pu@S~WIvm!J>d0VO$-B-_>!5p$0yFc<@7hO%L#|AwcbV7?lInaR#@Hx8hjI{@Y1+RZ%>}`Ao06;%uu1Oo8$ii4Q?#lN9?8H6dgoh6PTCZ48?{%jZVcsejk7Pefk$~pZ>*v?R)R@ z{e$x4a1YjO_p7WQ7BCk)2$yxf115`*lVGm6PKbJ0UTugyQJZ{?gXmM*)E zH5UdFP=f(Bm?9+RQk=QtC0JhWsVCPEu~N=Cvs7wtPim=H@MzY>oBj0j1$I?5WlagN z$G)$S1~S2Apa%w7>4hWn2atA!Hl#fVK_fh1<{Kbs>B{@(mMZrF?ua zP>g7p_=+Xm%o#$0RhqW1wy4HA=+|@K3(DG1Tzz7O3g&aPTXfIbs|9eKm1eA^ev@0*V*~aHZa7zXCAQmphyvoE-3q{J#?K6B_S9HA%B3;w-9V+E2Tqx6hxSS-1;cK?HPF zj|Kw3U>+1uSb7+|+^M~G#P#sJ?vw~#WvAp)~*+=z@2Y=tY!<^zc z1jG~&OJN3}sx5#hs@EGJH@&KFpmvi{Lr-9UvQf?2jFhabXLa589(nL%*xHZ~ebm|@ z!cK>a*oTnCPd!Ht*YcX)nm62U1wv& zBsgg=6|A_t+<&{GnYO!y8PmvKD^kU7L(NkutF4uKObyap;ifyNyLaT3!~NhE^UyjK z@zN^b9`?N4%o&#(uPAf7nx#cGakSLPm?;il2`7bSPq3OtiNwvH1KzjxQ?o5(SaxG8 zPW7$=Sr@HWo9kmJ$%`nr3n{#4N<=Sq44P&y>KPU6$enSlT{LG6e^vMY66x>C?^?>s z>8OgR2ykJMS0qmEd{Dsg_ydyRu|P`~X?o6wOoRdVK@CO$n?^kktp>D`+NM^nwN}=? zr3LloIcRIEIe=kNfy0K>VD7Fwe|>bw)y@}2Y%^FTNeEzIwzJ1oIm6IaGIE-qHH*6N z$GQF6+WZXbAv32zE?&7e57d$0z8;hRdaLvD6f@)THUHwFp3y#9;AmWF0&=@X5fveO z42}js&|oC9psMSD;ApR3Al~f@IoSvqKq9I8kqs^ZOT=quts{*UwgezRo%3 zKDPIIfR?_8xg;eOTlZzD&aQ5r4t=~py9Fy~fjz|K#s+iVJn!Vb8n6hdFTEo!!@U7L z;Q(t36Hu6z9f5_jqE4Ls3D~NUO-m?sC>t^&K7XE8*yVfc-4@6RLm&wuSqTIv>F%#*Lz3s3SBzuDu@ z+Hk_IB3Pw5-wyC=t7z5WWnLmx9iv~(@%+w*hkaZzL%P8UAxT7K5h=;%ydZt`SzHWw zrbJcn>OZmn5HnPiP=JUOy@n@{d-dI|qdO#ZGZ}MDJI*^n!p9E;=5GC-fF+JC8?bF) zCQ}VU00cCGSAg&k+Xis@^7h3Jz=omJEhww*4ZY&HGz}+^i4D-MdcpG6Kprj3Ty6=n>z!b z3to@pA}qZtP5}vje>?^UN;5MMc!V`;FXibu&17vWthPBD?2@3+d+sk(9q!AnlDCiwx z4l%cr_GN!~8)e`3{^|Hm;>9H%K6Cqy@mj-#KywIIHVE+Xfp*;>PDLeV6(!X{y!!fw z*R3SWh%ybXLf`-o@_0S`@a&3NxZr`FqKDrvFtfoHd6ab0(N=45^hj0Y-2;0}W6HGq zIbdO!MY(*Ei2fF*d9vs3lS9)$XagJ#V;BDo-(1|n3r3WSYm2Lx;T{gzBMk`$u*e=@ z(}7Jp4j+qyzKu#2&Y8R&WeJcA)R=*3wxx7dyxyM6cy6w69ork}0#=ukC2NUio`19-%jDRpO&R&xV zSbA+++{uS9crm>5Yab1`RCtkiD2hT=BIEg6%^|ou1k4j)%h7PgHTuW=@%t6 zhhsg)xUNon`?d8FRf=9xnD=o~Z#LFdIWTerir-BP?%uCm6l_kH2(zT%OJdm+fNQL$=8|Mdt;om->XjJ$K?RxFJ9@6dlp|mr(T-~UzsNUn z9?F=`czQGN`t;&_f5qcI@%)pL$#oY^j3KUtP10~a*Gs@l_j_8&7Dy1=#|3*1w-dJiAIWlm}Jx#oI5uh(BTzaM@*{_Wl0k3Qc1-@_iS;6SMkb>CCm$aQ1Z zYKO9~-C5~iA2YBmrExMQaB41upo>g`NHhozCEQqf%+d$S}j}|DNMN-u#R> zCn3G2K>)lI!sY`9QqPBue$X>(jwweKIcZ6@5M)Zy)^h zk>Biud!WDl>tYf0zVA@gRw(&P29jqH7xs8r2e@ZhT~{1dk9c)uQGv^%)$^t2y!3Wn z&bv#7nc<_u$Z$Q|^KP?{TK@q4f3EJ-bx7irSpEM2UEcGL|F56&M?NcfY?~_su_ePz z=zy1F)+rPq7?>SU5e-ftKuTGvkZZcN%=NM^$IYKreBerJ)eRr-dz{%MPcP>Uv=ORZ zEh$p^uNiD9~4w>pKH<04$wv)rSyfa~sQKR@lY1T=6o7RLdh@`3g(dsX>BxO1&Y zU5_7+J1=lvanDaGZAwAz-`wdCD1aVp9^%WZM*m{2j3cZLu(|*Wd+PZs&sJvG>8{|0k37ZI>ksR5g<>mJ)UStd>Pb&i`C&#v=0F2< z7{QY%88$ty^lg5<>Z8i;#Lfa-u~$E_eBe$p^$=|+qL!`I3cDz_WU%ak;stzWz__k} z3m~wYr}y=)M4WYDf$~64d8!f$lPOmag&?fGKG()t8a&MjS9tje>@oC_+b~G$Q;$In}ZiVe{!mQ@gu5dvDY z(C6WmK9&npEapQxBkN4{0T>_8zQtP=)4*m z3bP6ag;rp&^niQ!C!(&@>;+iis`CI*L$-NVxbAuY(ct3~-El#l?fK+{oOg40Eor$P zQ7@=Z`CX5>d^BcC(w9R}oY~ORKPRM=Wu1AE8gi)9mD_djD&8Jkto@E)zqaG>T?zAA zMCB$KE04QcpeprBiJ;%Y;~p;vuAqk%T2)}BS_NFuchK);RxOdIJfP^f2rRa!3%s$p zC?NJ{Y8ET%D#C#^aEQ`UclUneI-kDX^#bcC? z(AH9G)4a5cNdF4Y{$rn9e~80f(ZB8s?|a@h;Na8~$ZGrfRCjy?Xi=Z}VC_;s7VO%Q zwSGj>prC7aRERy5E45DR4mV?$oZd?o_M=nxw&6|9x{znMQU@bxno-vFKzw+ zA@M!v^TWXSGLzuQ$q+USS@wSX+RtCs?FoIVEjfn2KJjFB94L$+92ia!phGId8SU`u zx9{Tc#&~ARo_)kR0o9a`Pf`43p`mlI_4v1xnk!t3Dzd`|f3>q3uJaSOm7f6j_Ut63 zfSBHKf&x=WK>)F3rJ*DxE=qg1`5k`qRkdsFm-C!C^ITRtp-+Jq^Vy^0p6_k`;T^-p z`mS9AQ3pSBHeW0+e2R-VRjxdioGFP+s0$ny4#C_lKxBY$2&RgQAKg9i)kpd4+4-=dNPt9-r`HxXEbhv@LunHO&aMa_^d(r?WiX*t(;LvK+jU{xCyhCUR zk#iJRD?^#{JBBKnqum9>@pUd?+hLx0X~0Hoz4fpEFU5qg0cZ#Y0@V5c0kCTs(*ZJo z%OK(oPzac#CWi_Ig9o`n0I(6dGvm{8x23s5cU;0$zv)c%qHI)dlpcVA%sGCKhtCT!2*GejimM0SoZ}BGF921q3ZB zwD%>@kjda75h_>8#N-!m&guG`Cm3(orbbxmSPq^*qS7p_7FBb8y@dC1=4Z?B%o95H zwRUwleb2Wazh5`)@%wN7dL`CwJnk>(ha*l)!|d{n_8>?`QaJD)&4eCw=(d?-EetaFf9bc(!LbPsRCW^RuaZ+C~>t zVbAQZ`IYN^J)UW7v|r#dpG!Iz=PdVfdS56}5P&kUYit2qw<|xh5NqJah2;*}axCYx zz(8-kgPn`lxpK~C?#Ri=8i;Jm%Nb@~cYYS+MFz`0|o zg|RRK6r7?bCOE^HM|AD)-$A{DV(Aqwt~jpsT$xtt~KacIx?)rEyJvyp3rLmu%gO;iT z7sH1`P2SrUjRDrc(0Ib-N*^;<2Tp_CyAI<}je@;Q9c}~~bLU;p$*2Gn4#DFNVMvRf zqH-SF9IpZ;LXaRXN=vTAmv($9*S|E52X21B(3UH!cCAXabEGd(jdM_rb*7Z;S?}-e zoo_3;-U_%qd0KpE9$3{0olnAit%sL7{(w3MwR2h;+(y5J zgeyHD!wR{jzM5QG6Z(+407hMRa8=Qf3hj;wYth8_7k_K90dM|vT%OLl=Ckj50Z5ga zaYgFEW`BsZ=wZir+L!qg#_j9vqZIYBB1o&jS<2+AapQ8~8GoF=pif4Ws1RUp9xejd zHJ7I+1FOijT8sml8<^p=(%At*j~5(^7YzgQ^|lqOM-&ni+_3}UHhFL2!(2bi_?x24HX7i zdzHC(W^i=m*ZUlgIYLx1Y!=FTciwtC0I7JqtIQzho|0ddrE@j4+KfplchdV_VXP!B6tj?edBfBal$kMoDMFTct! z|Mlk!>nEv8}(N7>~7;ToG56a2v#w$13IEQqoag?7J@9X z6=)U|I?;4E&oS)X;6^ia6SFcob1lvCWUzxCW*O+gw1pCKI%vWw>8WDB`p3UXr=DwL zPIN;DRCKGM+B?CqHgKAMigy93Cv_IuQAmLdcYu7b49Y5Ac`I;P#gx@KUYFN%uYV#v zwpf%^g(;A>lhOF5WenY#=t=m<@0?nVqK z@c<%&k~wxmBaFz-+7kpN1UM`>GEUhIVnLaX?k-iczWai$fP}8QIjDjq?z^cOF6)CWH z+?XV2VP#YMr&9`L+DcN zsloE$oC}L#6LtL%*5tKd+|=b`~0tT?UxUWqw}8GFObV z=k&OV<^al&+9+IS-le!fx)}AQ1}#PVVJ!uId(^ncztQpXL>OH?1h zOSS+SLnDIjeD~Og)xf$zUYiF={Gl_G(;2XG7<1r#X>bVm*!yEwX}eL+Keb;8%Yb>Q zeZbc@4w?zqosJ5{fMEw$A%F?rPW8F7IR;zB=zQ*~%&}n8qV9K2R~kF3ryXq3<0p^H zT;@tuGoUz|@+r3o@Gc9JbZR^(D&C;>wrpp0fmKAFYVpcG4PG%E|Lx}E@1%#{a(b*U ztjC@G{TDeOP3&c?S(>Aq@u+UZgKN>VH!)?QPCu9e@bhS=_wcCZ1k&zqxxKE#j~VaQ zZC*IVry)9fxDggiJlD8d`s!Pt*n)6u167x~-`@T9zF+*}ho@ivFXKNsd>sAE%8VTj zFodHO8a;IwKpUt5YuBPHuUB*HMP_8hL_rCQDNku?eu$Q*2zsj2tw;7?>s7Te%}(&a zglomU$GuH;4FNk70YJ)zy!8wJ6{r90cjZm1?Qg%g@>Bk=G0xxqqrHBVH(uC=bv!C_ zyb4OtVOlK9anC(=;DjVeU0`vyiGI>Mj5V)DNE&&d~_25PJUaa>gFXRvqa& zG9d2S@jVH-6W>p|J9l>{S~Z{-NEv-Na80O6?x?OSRK|*8Vhd=dT7@asPU>5Uzhh0? zziw6CN(fOkvQ#;%4d{ZG@#kM?^~|u(UvmM06QqS7{VfjT=aBlXEEo?j^ttRCXLUE< zEJ{7N=U90easIeI{PA+s^GpneWPT;;WB$Q7aE0Tk5jRvKn+*$hG9Fx{&RWiQE2#)5 zXVl8bMFTo45J~ZRDAV9-cJ=-1%wDe}5YPcJjTX9;$*g#S_C26;N2zQs6t=xmmZRnCql&sIwV} z7n8Q1;wtZ-^Z$tTxA;+LtlS|p%2WZUqVsveBnl9h!Ud{J^tWgBmO8BF(bU>EpTh=z zj*PV%Z^wms#&{p0*E;hjjQ?rpliIswZyxivdTOx(<-Kw>7bxYxb!-)51)L5OGcm)m zo`)`=&1{fF?Ce%Y2RWonz2VOjSdb-~0H{)z@zla@@C)jsEbgcM+QWNnkB=YL>RM-8N;b>nUgic^R3Iv8IXtqCd#pt|U6|8XXU8mUO#SZ2 zH@Hr4zJ2#!B?_P#4WmRXYW32P)&re)kjpH~1??sH4gX;r0gOcO!)tp9^cQW9{UzkQ z%{z4W))4Qo%z7d4U!s8#ws?F~Do`v6>wwJcY0b<}qmP`F6Ze{`Ro6Z0X^7OgQN>^+_`_|V!x-Giti zV4&ecy_imI7)O-}Jp&m8Q(`U=(PmG2*%eF<3M+DI_a3;T1Lxk2zCYxvzJEskYk6-G zg%%d7A`EbkMme~j$sGplRZ>793wzpiBb^PH$^`I-Z|wV4{pQjbY2v&T&b3*!An3xo zOT9)^#m!$Yn9QHfs+9s#Ptbj(N_+Oj`g$(Z%h)54kH8KMwm~2H`kZ$`A~L!;s)-;W z7On^iKVQv>l=cQVE%p=d2*`T4o#2(E?T*YkDoR!arof5 zUYdT|304xNFcr33#ECFw2>}!}6<;u)<@qY{!T61vmYWToEJTK& z&~6T`n`)t7)Oq66&LUjimY)w83T1uH$U4P(zbrMHgeCS~T5uS+n_=R0 zZcG3F_xMI1D_0;$7F8XAx}vt(g>ngSKrJP8Sm!ZLwm<)+hd;FMzjN?j2(AVOGP>7m zr}h3gR*EigngJ`*M$20GF!|cpA;+_L|Ea{O*D-QttZD+zWW>0s=22y*JToM5mpw#f z->i>apY2N9L#gB+wUbEBO-g*Ct?l|R! zT*+Pf9BN?)I?sg^|B|v1{1f`}`G*fb@;HSu2Yw#!_i^4lc1J$F+aDP)FKBZctU3HP zL~nbSGFqMeR2Y(Sy*nO5=Xoq>rM*VaXliy?a0k5o=C2TTg5ZAe5Cge};{ zo2Z&ewG=VtSqe7KPma=9V6eZ~+|F5inZ^w+o~hUf2Su5+h2cE2Je;NLv>x z2Xeg86E80IGSrUpsQL^UdO6@r!HNkGOqp^!?V|4H{?2{5lc?kpmzL@;0wxB>mi_hy zy^P4m3IaoC=cI0QE9M*_5{Im!af@%T3OAI@LO?`>r~{3&3AiM!J?g~t>S8TTF)NA@%%CbAPMxG8vO4M}_^C#uOxJ0*SoQyhGbyfm6z)dQ?iz&sAcqdDT=-wFbh zDHjO9MMv%v+4FwFV;x>#i9yFbZuRxeuRVRuHM|7|d67_qhJPY^h#Lis?Y6f!_h~B^eAt;w?!Bp#={bJns1j|$)Gjp<*GtlTR?iwCu zvXf&(j*Y{>*vl(0JC``ATaXAiKpaGjDnjmBvj$91L!kDu@Z-rg zp4-?s)2KP;XlBBTz{l*bqcA#TkMb zRlRCu_e8_9c;z$F`P^jW5Jg-sdtXpkT zR##FKo@yY1x(+1+O@Y?1GwW-l`mQ`i$gLD>7%S#9u}m)WIE{M0Q2TSw^A703Vt3a8 zQYv}g33h(`YCiicx0eI72};9mHEfz@2Bw1r7e~*C=vJDiSd$CFPWcHQy>b6^(?>WT z9D&dO`(NZJPN}Our8bzt*m5%fKA?fv0<6$$Lxh3fwO`&oZ@G^Thf)-rP{0-QY(=pk z>dyenbhf-0Ck9qrd+eRo8NgHpS_CgPgPCNP{R-r=dI5yWVhiP}BadH_-ZF(+_~H8- z$a}Q8!_eolTI#Oue0T8V{Ww#SY#>YKL{uknUHaF@ zKig(+Z-C0T7MkU`pn;kPZoi)A!RqK|e)?6XFRE|ePI8Iw4osh~$9^Gi^KO9nYJSXE?!n6SK z0F-Rjps6GHjgS0nJZxfH9K3v(=Xcxfw%xPbVrlX@$jTu+K^{gt4(hWOdS08*6Ip{C z)CbtsGS+1cLmtl;GWL=j+gN=3;C*}k+7H8^x^tOnQ0 zag0~*bfOC=-ADDQFqWoeQCcJv?)Q1=RQgGUR4u$(oK^Py1W>%MI~{Bgj%u-$&knDk zdW5?rrX|^d;!v6t3mR%v$MF^9HV&=b28`JET4fdCOVwti=7T^301u+4M)uBvx~bRS z`f*u6DaCZw)_eW&_ao(i0_&=h;_Dz8Zj_M0|Ef%x8BS5AMYQu zH{M5j`(PKX@AJV6y+LwQOiR0xvt`BHs*762^55W-5AiiT8GSA;lD1|>OGZxumcU?B|To_R4Q#W;yk z{Eb*rHw5`bh3EvCs6h<`>B!+x7Lfv_UfoZh+~B@h=5kod4&UzPyeAhCsVI`N4zfeJ zt@CXcM|x5^2NpTC9btiR7`tA*hdpUs?|1)k#Ih-xhgBuhSw8w;8p|PVh&n_EqCuIm zB0>NO0z#^3fG4e>dyREpm_{Hom#S1ddQ*!xfUyGufbm+s^>%oZS1%fkHe_djrS*N3 zWe81BQ)RRbMQB*{=hG4Fflb)JK!{zWSkh9>vLVfhv4SoE19D<)%AskqJ9L0BEL!6Q zcu`s041|o_uiJ;T9_3G-9{brRniu@lUr?bOm@N`$RCOq2&8n%hPhR0W9#1JWstg{ssSU&n>2FddOOquoNn(vEVx0O&9U;;7!TWmeAl365Kt#4$iz0WS~`(B{bB6G z;HGx&M{D>2)kPD_ay83@7{gz|9|$K|MwHO|M=Z*MN=3-cQ55^cH?e)ZO<+~Jp0yD$w~70 zXC?$#tAMDW5h`#}%=Oyc?!)3@e#_NGf4lQ}O~W(_u<-kO#gX;aJ9P8D#?Tt}$SzR; zz4Q6oKS)NHkY|L!V;{PWNNCExU_}IGgA-L?JXG(&C4k0nzm3CN_+BtM#)vHmsz7|6 zH6+b$+OlnLZf_;HG;JneJ@|mjLPkmvRA)yjx*-+Nq})l>RE-2+h(_paf(T?083RsXH`u-RGYl@wiO;MgcPo=>of-hM7eu1ee|wW;WED;*4c3tz;I-k z*=e6^i5{;$Udndr%O21B-`DYLu0tm|H*2xTajeguuOQGbkXr^&474EL!9nZFQ1{21 zrZ+%W_)6rpLvt;j00S^lTcJzbJx&PF)4r_3M>i#J`sk?o7kHjrtJSe*@RYX9(Y+8+ zvSzc)f$`;S6XSRzlr35`<13W%3cro89(&gk2j=^_%*Qx7JN!kA?~P?yFe8VKfHqMG zT7e)c09!h$K6kvIJiWIs*&Rm*S~>G0qaQ~9stwHfd4Bx1c7)1f^=J>xL65u=E z>qJg=a3)hN+jN&LD>_c*O%#)d!1LGf`PxJoZw{rYJ|n!$-$eU^&pp9oMYU*=EIi|> zwG_Jt!HNG_e)$Juk(v=OZVE+*nu8}10X><0{CFYnqP@ewtT$Y0Hs8LyT+!?%*JDF1 zppoiMX5!ImMlY3d4hQTdyIcA`qkd-L#O?P$qDxX=-iYhn_UJ2k$q!uM_bQg>a#+xEF#PxoPDlEei#@i-lZdgC) z;rkNHL_YD1d~`bgvk!eQ|NP#nTATNtIy{g(5q5$&JEQW)99E(tO6f|Ed*ASBetPWL z=dpV{Jp@fd9f8~kxfyIEh<9QRP0ZzaGc5(2vtKnka;YU_r+Fz=#U6RkWMxn2gw_)v zq)`UniwR=}qo~l35b?q5w1S{3XaB#xT)sAw@10f zskO7uX@rr{2v>Dtf{t#K`=Q%wg5y6O#fPOocC1aD?oMtcAgC28=IF0i#&}0nr%%t) z`L34=)^+LQNU&z6EC{_E+a;3al8XutFkW$7z&*GH6tK5@eaZ&25>@K-RLb)b@|-Vl z*I=M|Xq5%Na*R=+6%m)&Cyk=pM zQ=cDFm?2DNCh8D}JbzgxiWIerK@^4&ul?#fS2HS@@q2f*;EmH-Dp z3AZCNJM=WCJrDBOXTg@T*;35I?J&NLr9r)t(-m{0#ay|EY4=NrPX(DB4!`+oYf!152_Y7yRHHq_sWf_5ePn|<;KSc zSTO2^bPjluqPLxl3O|#CcQq!!I~R*N-#M6&>B^{{Q~^UdW@VM`={m8#}e^nFPnM72upv4kx$2po0NOL94-4UnHzdkRV z$G3SZ_SBC(t7Wn+TQM0qG`gkM4Rfcrt}_Cd-g-U|nuI?8@BdrQ{7V$&clrXV)wEyL z$x>;EyYb}e@4Vk2N9NqMhkX93`1$nLAAV7zdthYOliJKwUf>z3^vt!icR+^WH(IRM z@z-_TzPgXI59Rr`IgOn9@W{3two4D=B{Ma~ta{kXo@FI5@m;_gc}N^oHX`v;&>2kv;1!WcaaR?0MC zdFP0OU2{Z{z*sak+W0tXskVF2)A9ZLuWXjh(`*Ykv0UQj6?s$@qCz{*{6=28;Ak3gQyOTT}BWPS1>J?!Nk za5L8+*Os{>MZFv)o7Q zmZZ25hE7v9E{YL=Ew0khm)*-4ep$Yv3}u30!PIV0Sr_@R7_=EhDQnDE%2fyyP$kK&i}H~M zGqcwE@Hps$yaE7EK(N0KN%};Fk^#(lrC~cf@mY?$ zfc=f<5=@16d?f-A{@?%iBj-r6epTC#iypC$;x1&G&@{T0&S|4i>NCFa8RpB@U8qWf z&Sj6;@%T^hJ0GQUyrQ)j$=V<{8SC z6uKAZ*?vzs0We+kZdUCEDpBT)XTGv8$R7oIY1@^5MJS=g0KEqJuF4f$m#cuJ?u``0 z4ba{pn3_S*DZPNa9oFvC^CzE`E}Z9U%Z`-Mi{hFCmGH-Z`#%y`M+ioDw#ShZPe@|{ zk%)2I3d*kOnn-8ZJr!r;m>KxDuVKzTSx4ddw$ph|@m%<|#r7c{4Se3NG>Tr`Z3@G`b8{DYA@hMoYF- zulegKNM8s-lodUVE@1bi36&{CRZmwjm4)V6F1FX668;g&qc$&X!=&yXK4#NEP}q&* z*uq6m0|h>Y9JIKD!Z6l)x4v}QNbB3ti9+tuyR5Kl78J}sPH#HUCR&(k!XQl2bYR*m zpD%eJedw^;w?)Eu53mDryErojQ}fFGowtX*ZnNLxoSqvQ(X9GrKxr=6srC0+|>BOacWvvTBQLIF9ZLKl7CSg!~9` zC_{}SKMxCRp2eXcsCP;MDIU8WYdmNkW?8b|oni6w-2bL?ya%OrD{`)ta|HvM>2HyN zIykrhjI#(cwi()tQQ7?rF3zNB3IFf^&QDm@9V|sY91!#h6_nmyoYxhn0jz{g6}G+D z8PK?qw`Cd6a|>FQC;y+O)1I%~xuw4hqCgYU}%rDVLXShzbFO&is!TutRPV)U&1eEgmpyBMd=tV#^< z`6APHbRBAyX#fE^OYiLcIy#qd2*~*eX>^S`KH@2dm=W5Z=iF>*C10xH_?9I zs=7uXVw1Brqa?HFiv#frQ@6LnuV+$L4n%ZZ!bN6n^Z0m=jYW+NSSS=gNT+HJVg+;# zc$<`7-LsEEMeao$3I{SNP0U~x5{HJ|^zGW2-&yhM`tl--6$+q;HpU|^fm038P>Txi z9+@3W1$Bi|U7=qXtroCT+lLNjLPXWz0ZtQn3hmda=J})bA07J-w%ROO)h0gqbS|J- zlrh+ZO)E4rLlja8hk;G`KuyGacv`1Z?irBS2u3CpImJaccP@%(uXZDl5U1UK8PD{B zlCgSa4@$m1SGCGK^&A<-fIHU>?)VEoU#H}1J!az=s0khkDC=@TP`N$OH5mⅈlR` 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 = { kiloex: "/logos/kiloex.png", orderly: "/logos/orderly.webp", backpack: "/logos/backpack.webp", - // Perp DEX valuation bench (perp-pf-ratio); icons from DeFiLlama. - avantis: "/logos/avantis.webp", - apex: "/logos/apex.webp", - derive: "/logos/derive.webp", + // Perp DEX valuation bench (perp-pf-ratio); marks from each brand's own site SVGs. + avantis: "/logos/avantis.svg", + apex: "/logos/apex.svg", + derive: "/logos/derive.svg", drift: "/logos/drift.png", vertex: "/logos/vertex.png", edgex: "/logos/edgex.jpg", From 620990ae9792470d69c3e6315eb582e50502c2fd Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:20:18 +0200 Subject: [PATCH 3/8] bench 265: token-less rows report harness health (were shown as UNRESPONSIVE on staging) (#2350) Claude-Session: https://claude.ai/code/session_01L4rjxemTvgRwrCG6D2iK5D Co-authored-by: Flotapponnier Co-authored-by: Claude Opus 5 (1M context) (cherry picked from commit 7bdd0cf962a61b986e9a04e9e3be6492f78df77d) --- benchmarks/perp-pf-ratio.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/benchmarks/perp-pf-ratio.yml b/benchmarks/perp-pf-ratio.yml index 7ded18255..5f84660cb 100644 --- a/benchmarks/perp-pf-ratio.yml +++ b/benchmarks/perp-pf-ratio.yml @@ -50,7 +50,7 @@ methodology: - "Revenue share = 30d revenue / 30d fees. Adapters that publish no revenue series, or a flat zero, leave P/S and revenue share undefined rather than zero." - "Open interest: DeFiLlama open-interest overview, perps product only. Fees to OI = annualized fees / open interest." - "Drift's DeFiLlama perps adapter (drift-trade) currently reports zero fees; the row stays in the cohort with every ratio undefined until the source recovers." - - "Rows without a P/F (no token, or no fees) read their success from the P/F gauge itself, so they rank as unavailable on the headline and are excluded from the best / median / worst strip while still appearing in the fees and open interest tabs." + - "Rows without a P/F (no token, or no fees) have no headline value: they are left out of the ranked ledger and the best / median / worst strip, and appear in the fees and open interest tabs only. Their harness health is still reported so they never read as unresponsive." - "Poll cadence: DeFiLlama and CoinGecko every 60 minutes. Prometheus scrapes :2112/metrics every 60s." findings: @@ -261,7 +261,7 @@ providers: p90: perp_protocol_pf_ratio{protocol="drift"} p99: perp_protocol_pf_ratio{protocol="drift"} mean: perp_protocol_pf_ratio{protocol="drift"} - success: perp_protocol_pf_ratio{protocol="drift"} + success: perp_protocol_health{protocol="drift"} sample_size: perp_protocol_fees_30d_usd{protocol="drift"} series: perp_protocol_pf_ratio{protocol="drift"} @@ -339,7 +339,7 @@ providers: p90: perp_protocol_pf_ratio{protocol="ostium"} p99: perp_protocol_pf_ratio{protocol="ostium"} mean: perp_protocol_pf_ratio{protocol="ostium"} - success: perp_protocol_pf_ratio{protocol="ostium"} + success: perp_protocol_health{protocol="ostium"} sample_size: perp_protocol_fees_30d_usd{protocol="ostium"} series: perp_protocol_pf_ratio{protocol="ostium"} @@ -352,7 +352,7 @@ providers: p90: perp_protocol_pf_ratio{protocol="pacifica"} p99: perp_protocol_pf_ratio{protocol="pacifica"} mean: perp_protocol_pf_ratio{protocol="pacifica"} - success: perp_protocol_pf_ratio{protocol="pacifica"} + success: perp_protocol_health{protocol="pacifica"} sample_size: perp_protocol_fees_30d_usd{protocol="pacifica"} series: perp_protocol_pf_ratio{protocol="pacifica"} @@ -365,7 +365,7 @@ providers: p90: perp_protocol_pf_ratio{protocol="extended"} p99: perp_protocol_pf_ratio{protocol="extended"} mean: perp_protocol_pf_ratio{protocol="extended"} - success: perp_protocol_pf_ratio{protocol="extended"} + success: perp_protocol_health{protocol="extended"} sample_size: perp_protocol_fees_30d_usd{protocol="extended"} series: perp_protocol_pf_ratio{protocol="extended"} @@ -378,7 +378,7 @@ providers: p90: perp_protocol_pf_ratio{protocol="edgex"} p99: perp_protocol_pf_ratio{protocol="edgex"} mean: perp_protocol_pf_ratio{protocol="edgex"} - success: perp_protocol_pf_ratio{protocol="edgex"} + success: perp_protocol_health{protocol="edgex"} sample_size: perp_protocol_fees_30d_usd{protocol="edgex"} series: perp_protocol_pf_ratio{protocol="edgex"} @@ -391,6 +391,6 @@ providers: p90: perp_protocol_pf_ratio{protocol="paradex"} p99: perp_protocol_pf_ratio{protocol="paradex"} mean: perp_protocol_pf_ratio{protocol="paradex"} - success: perp_protocol_pf_ratio{protocol="paradex"} + success: perp_protocol_health{protocol="paradex"} sample_size: perp_protocol_fees_30d_usd{protocol="paradex"} series: perp_protocol_pf_ratio{protocol="paradex"} From bf375f036c70ac8545cbb98c70e89a661e198110 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:43:49 +0200 Subject: [PATCH 4/8] bench 265: pre-TGE perp DEXes listed unranked with fees and OI; edgeX and Paradex ranked (#2358) Tom asked for the pre-TGE venues on the P/F bench. New spec field provider.unranked ("Pre-TGE"): the loader stamps unrankedLabel on the row and keeps ms at zero, so every ranking helper (liveResults, leader, field stats) still skips it, and the ledger renders an unranked block under the field with the row's companion-panel values (fees 30d, OI) and a neutral badge, sorted by fees. Ostium, Pacifica, Extended plus two new venues, Nado (Ink) and Decibel (Aptos), use it. edgeX (EDGE, CoinGecko rank 167) and Paradex (DIME) turned out to have listed tokens: they get cgIDs in the harness and enter the ranking (edgeX at ~3.6x). DeFiLlama's fee adapter reports very low fees for Paradex; noted in the tag and methodology. Verified end to end: harness built from this tree (20 protocols, one live poll), scraped by a local Prometheus, materialized with the worker's loader, rendered by next dev: 14 ranked rows, 5 pre-TGE rows with fees, no UNRESPONSIVE badges. bun 261/261, typecheck, validate. Claude-Session: https://claude.ai/code/session_01L4rjxemTvgRwrCG6D2iK5D Co-authored-by: Flotapponnier Co-authored-by: Claude Opus 5 (1M context) (cherry picked from commit b9f7cb142550b4c52393f8e4670a8523a6f63168) --- benchmarks/perp-pf-ratio.yml | 66 ++++++++++++----- .../perp-token-metrics/cmd/script/main.go | 10 ++- public/logos/decibel.svg | 6 ++ public/logos/nado.svg | 6 ++ src/components/ledger-table.tsx | 74 +++++++++++++++++-- src/data/provider-registry.ts | 12 +++ src/lib/logo-manifest.ts | 3 + src/lib/materialize/load.ts | 21 ++++++ src/lib/spec-schema.ts | 12 +++ src/types/benchmark.ts | 7 ++ 10 files changed, 190 insertions(+), 27 deletions(-) create mode 100644 public/logos/decibel.svg create mode 100644 public/logos/nado.svg diff --git a/benchmarks/perp-pf-ratio.yml b/benchmarks/perp-pf-ratio.yml index 5f84660cb..aca79905e 100644 --- a/benchmarks/perp-pf-ratio.yml +++ b/benchmarks/perp-pf-ratio.yml @@ -4,7 +4,7 @@ slug: perp-pf-ratio number: "265" title: "Perp DEX price to fees ratio 2026: Hyperliquid, Aster, Lighter, GMX, GNS live" seo_title: "Perp DEX P/F ratio 2026: Hyperliquid vs Lighter vs Aster vs GMX vs GNS" -seo_description: "{{best_name}} trades at {{best_p50}}x annualized fees, the cheapest perp DEX token. Market cap and FDV against trailing fees and revenue for {{count}} perpetual DEXes, live from DeFiLlama and CoinGecko." +seo_description: "{{best_name}} trades at {{best_p50}}x annualized fees, the cheapest perp DEX token. Market cap and FDV against trailing fees and revenue for {{count}} perpetual DEXes, plus the fee base of pre-TGE venues, live from DeFiLlama and CoinGecko." subtitle: Market cap divided by annualized trading fees (trailing 30 days) for every perp DEX with a listed token, alongside FDV, protocol revenue, float and open interest. Lower means the market pays less per dollar of fees. Same convention as the DeFiLlama P/F ranking, updated hourly. category: Trading @@ -14,7 +14,7 @@ unit: x higher_is_better: false disclaimer: | - P/F = circulating market cap (CoinGecko) / annualized fees (DeFiLlama dailyFees, trailing 30d x 365/30), on the DeFiLlama parent the token accrues from. Fees include the share paid to liquidity providers; P/S uses protocol revenue only. Low-float tokens show a much higher FDV/F than P/F. Venues without a token publish fees and OI, no ratio. A low multiple is not a recommendation. + P/F = circulating market cap (CoinGecko) / annualized fees (DeFiLlama dailyFees, trailing 30d x 365/30), on the DeFiLlama parent the token accrues from. Fees include the share paid to liquidity providers; P/S uses protocol revenue only. Low-float tokens show a much higher FDV/F than P/F. Pre-TGE venues are listed unranked with fees and OI, no ratio. A low multiple is not a recommendation. seo_intro: | The price to fees ratio for perp DEX tokens measures how many years of current trading fees @@ -50,7 +50,8 @@ methodology: - "Revenue share = 30d revenue / 30d fees. Adapters that publish no revenue series, or a flat zero, leave P/S and revenue share undefined rather than zero." - "Open interest: DeFiLlama open-interest overview, perps product only. Fees to OI = annualized fees / open interest." - "Drift's DeFiLlama perps adapter (drift-trade) currently reports zero fees; the row stays in the cohort with every ratio undefined until the source recovers." - - "Rows without a P/F (no token, or no fees) have no headline value: they are left out of the ranked ledger and the best / median / worst strip, and appear in the fees and open interest tabs only. Their harness health is still reported so they never read as unresponsive." + - "Pre-TGE venues (Ostium, Pacifica, Extended, Nado, Decibel) are declared `unranked` in the spec: they sit under the ranked field with their fees and open interest, are excluded from ranks, leader claims and the best / median / worst strip, and gain a ratio the day a token lists." + - "edgeX (EDGE) and Paradex (DIME) have listed tokens and are ranked. DeFiLlama's fee adapter reports very low fees for Paradex, which inflates its multiple; treat that row with caution until the adapter is fixed upstream." - "Poll cadence: DeFiLlama and CoinGecko every 60 minutes. Prometheus scrapes :2112/metrics every 60s." findings: @@ -153,8 +154,8 @@ faq: a: "Fees are everything traders paid, including the share that goes to liquidity providers, vaults or referrers. Revenue is the part that reaches the protocol, its treasury or token holders. Hyperliquid routes almost all fees to HYPE buybacks so its P/F and P/S are close; GMX passes most fees to GLP and GM depositors, so its P/S is well above its P/F." - q: "Why does Jupiter's fee number include more than perps?" a: "JUP is one token for the aggregator, the perps exchange, lending and launchpad. Comparing its market cap against perps fees alone would overstate the multiple. The bench measures every token against the fees of the DeFiLlama parent it accrues from and publishes the perps-only figure separately." - - q: "Why are Ostium, Pacifica, Extended, edgeX and Paradex in the table without a ratio?" - a: "They have no listed token, so there is no market cap to divide. Their fees and open interest are published so the cohort shows the whole perp DEX fee landscape, not only the venues with a token." + - q: "Why are Ostium, Pacifica, Extended, Nado and Decibel listed without a ratio?" + a: "They have not launched a token yet, so there is no market cap to divide. They are listed unranked, below the field, with their fees and open interest: that is the fee base a future token would be valued on, and the row gains a ratio the day it lists." - q: "Is a low P/F a buy signal?" a: "No. It can mean the market expects fees to fall, that fees were inflated by incentivized or wash volume, or that the token has no claim on the fees. Read it with the float, the revenue share and the month over month fee trend, then with your own research." - q: "How is this different from bench 234, the perp DEX P/E ratio?" @@ -332,8 +333,9 @@ providers: - slug: ostium name: Ostium - tag: "Ostium, no token: fees and OI only" - formula: "No listed token; fees and open interest published, no ratio" + tag: "Arbitrum" + unranked: "Pre-TGE" + formula: "No token yet: fees and open interest published, no ratio until a listing exists" queries: p50: perp_protocol_pf_ratio{protocol="ostium"} p90: perp_protocol_pf_ratio{protocol="ostium"} @@ -345,8 +347,9 @@ providers: - slug: pacifica name: Pacifica - tag: "Pacifica, no token: fees and OI only" - formula: "No listed token; fees and open interest published, no ratio" + tag: "Solana" + unranked: "Pre-TGE" + formula: "No token yet: fees and open interest published, no ratio until a listing exists" queries: p50: perp_protocol_pf_ratio{protocol="pacifica"} p90: perp_protocol_pf_ratio{protocol="pacifica"} @@ -358,8 +361,9 @@ providers: - slug: extended name: Extended - tag: "Extended, no token: fees and OI only" - formula: "No listed token; fees and open interest published, no ratio" + tag: "Starknet" + unranked: "Pre-TGE" + formula: "No token yet: fees and open interest published, no ratio until a listing exists" queries: p50: perp_protocol_pf_ratio{protocol="extended"} p90: perp_protocol_pf_ratio{protocol="extended"} @@ -370,9 +374,9 @@ providers: series: perp_protocol_pf_ratio{protocol="extended"} - slug: edgex - name: edgeX - tag: "edgeX, no token: fees and OI only" - formula: "No listed token; fees and open interest published, no ratio" + name: EDGE + tag: "edgeX, parent scope: perps" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" queries: p50: perp_protocol_pf_ratio{protocol="edgex"} p90: perp_protocol_pf_ratio{protocol="edgex"} @@ -383,9 +387,9 @@ providers: series: perp_protocol_pf_ratio{protocol="edgex"} - slug: paradex - name: Paradex - tag: "Paradex, no token: fees and OI only" - formula: "No listed token; fees and open interest published, no ratio" + name: DIME + tag: "Paradex; DeFiLlama's fee adapter reports very low fees for this venue" + formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" queries: p50: perp_protocol_pf_ratio{protocol="paradex"} p90: perp_protocol_pf_ratio{protocol="paradex"} @@ -394,3 +398,31 @@ providers: success: perp_protocol_health{protocol="paradex"} sample_size: perp_protocol_fees_30d_usd{protocol="paradex"} series: perp_protocol_pf_ratio{protocol="paradex"} + + - slug: nado + name: Nado + tag: "Ink, Kraken L2" + unranked: "Pre-TGE" + formula: "No token yet: fees and open interest published, no ratio until a listing exists" + queries: + p50: perp_protocol_pf_ratio{protocol="nado"} + p90: perp_protocol_pf_ratio{protocol="nado"} + p99: perp_protocol_pf_ratio{protocol="nado"} + mean: perp_protocol_pf_ratio{protocol="nado"} + success: perp_protocol_health{protocol="nado"} + sample_size: perp_protocol_fees_30d_usd{protocol="nado"} + series: perp_protocol_pf_ratio{protocol="nado"} + + - slug: decibel + name: Decibel + tag: "Aptos" + unranked: "Pre-TGE" + formula: "No token yet: fees and open interest published, no ratio until a listing exists" + queries: + p50: perp_protocol_pf_ratio{protocol="decibel"} + p90: perp_protocol_pf_ratio{protocol="decibel"} + p99: perp_protocol_pf_ratio{protocol="decibel"} + mean: perp_protocol_pf_ratio{protocol="decibel"} + success: perp_protocol_health{protocol="decibel"} + sample_size: perp_protocol_fees_30d_usd{protocol="decibel"} + series: perp_protocol_pf_ratio{protocol="decibel"} diff --git a/harnesses/perp-token-metrics/cmd/script/main.go b/harnesses/perp-token-metrics/cmd/script/main.go index 649abe83d..6d32cbd23 100644 --- a/harnesses/perp-token-metrics/cmd/script/main.go +++ b/harnesses/perp-token-metrics/cmd/script/main.go @@ -43,12 +43,16 @@ var protocols = []protocol{ {slug: "orderly", llamaSlug: "orderly-perps", cgID: "orderly-network"}, {slug: "synfutures", llamaSlug: "synfutures", llamaPerpSlug: "synfutures-v3", cgID: "synfutures"}, {slug: "derive", llamaSlug: "derive", llamaPerpSlug: "derive-v2", cgID: "derive"}, - // Token-less venues: fees and open interest only. + // edgeX (EDGE) and Paradex (DIME) listed on CoinGecko; verified 2026-09-14. + {slug: "edgex", llamaSlug: "edgex", llamaPerpSlug: "edgex-v2", cgID: "edgex"}, + {slug: "paradex", llamaSlug: "paradex-perps", cgID: "paradex"}, + // Pre-TGE venues: fees and open interest only, shown unranked on the + // bench so the fee base of a future token is visible before it lists. {slug: "ostium", llamaSlug: "ostium"}, {slug: "pacifica", llamaSlug: "pacifica", llamaPerpSlug: "pacifica-perps"}, {slug: "extended", llamaSlug: "extended", llamaPerpSlug: "extended-perps"}, - {slug: "edgex", llamaSlug: "edgex", llamaPerpSlug: "edgex-v2"}, - {slug: "paradex", llamaSlug: "paradex-perps"}, + {slug: "nado", llamaSlug: "nado", llamaPerpSlug: "nado-perps"}, + {slug: "decibel", llamaSlug: "decibel"}, } func (p protocol) perpSlug() string { diff --git a/public/logos/decibel.svg b/public/logos/decibel.svg new file mode 100644 index 000000000..856129f19 --- /dev/null +++ b/public/logos/decibel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/logos/nado.svg b/public/logos/nado.svg new file mode 100644 index 000000000..8f24ff696 --- /dev/null +++ b/public/logos/nado.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/ledger-table.tsx b/src/components/ledger-table.tsx index 8f1b28963..386d02897 100644 --- a/src/components/ledger-table.tsx +++ b/src/components/ledger-table.tsx @@ -261,10 +261,29 @@ export function LedgerTable({ .filter((r) => r.unresponsive) .sort((a, b) => b.successRate - a.successRate); + // Spec-declared unranked members (provider.unranked, e.g. "Pre-TGE" on + // a valuation bench): no headline value by design. Listed below the + // ranked field with their companion-panel values, ordered by the + // first panel-backed column so the block reads like a mini ledger. + const unrankedRows = (() => { + const rows = results.filter((r) => !!r.unrankedLabel && !r.unresponsive); + // Sort key: the first panel-backed column that actually has a value + // for these rows (ratio columns are empty for them by definition; the + // fees column is the one that carries their story). + const col = customCols?.find( + (c) => c.panel && rows.some((r) => colValueW(r, c) != null), + ); + return rows.sort((a, b) => { + const av = col ? (colValueW(a, col) ?? -Infinity) : 0; + const bv = col ? (colValueW(b, col) ?? -Infinity) : 0; + return bv - av; + }); + })(); + const sortedAll = [...results] .filter((r) => { - // Unresponsive rows render in their own unranked block below. - if (r.unresponsive) return false; + // Unresponsive and unranked rows render in their own blocks below. + if (r.unresponsive || r.unrankedLabel) return false; // Sample-health gate. Rows tagged "insufficient" by the load path // (sampleSize < 0.1 × expectedN) drop out of the ranking entirely // so the leaderboard cannot assert a position from a wildly @@ -620,6 +639,35 @@ export function LedgerTable({ embedKind={embedKind} /> ))} + {unrankedRows.map((r) => ( + ({ + v: idx === 0 ? null : colValueW(r, c), + unit: colUnit(c), + }))} + series={[]} + sparkMin={sparkMin} + sparkMax={sparkMax} + color={colors.get(r.slug) ?? "var(--color-ink-soft)"} + benchmark={benchmark} + embedChain={embedChain} + embedRegion={embedRegion} + embedKind={embedKind} + /> + ))} {unresponsiveRows.map((r) => ( 0 ? ((value - fieldValue) / fieldValue) * 100 : 0; const deltaSign = deltaPct > 0 ? "+" : deltaPct < 0 ? "−" : "±"; @@ -724,7 +776,7 @@ function Row({ {/* Unresponsive rows never take a rank number: best/worst SEO copy and badge claims are computed from healthy rows only, and a numbered dead row would contradict them. */} - {isUnresponsive ? "—" : String(i + 1).padStart(2, "0")} + {isUnresponsive || isUnranked ? "—" : String(i + 1).padStart(2, "0")} {/* itemScope/itemType marks each row as a named entity so Google's knowledge graph can link the leaderboard back to that provider. @@ -811,6 +863,14 @@ function Row({ )} + {isUnranked && ( + + + + {r.unrankedLabel} + + + )} {isUnresponsive && ( @@ -984,13 +1044,13 @@ function Row({ )} - {fieldValue > 0 ? `${deltaSign}${Math.abs(deltaPct).toFixed(0)}%` : "-"} + {isUnranked ? "—" : fieldValue > 0 ? `${deltaSign}${Math.abs(deltaPct).toFixed(0)}%` : "-"} - {r.successRate.toFixed(2)}% + {isUnranked ? "—" : `${r.successRate.toFixed(2)}%`} - {errorCount(r)?.toLocaleString("en-US") ?? "—"} + {isUnranked ? "—" : (errorCount(r)?.toLocaleString("en-US") ?? "—")} diff --git a/src/data/provider-registry.ts b/src/data/provider-registry.ts index 61d21a28f..9abea557b 100644 --- a/src/data/provider-registry.ts +++ b/src/data/provider-registry.ts @@ -1772,6 +1772,18 @@ export const PROVIDER_REGISTRY: Record = { "Derive (formerly Lyra) is an options and perpetuals exchange on its own OP Stack rollup with an off-chain orderbook and on-chain settlement. DRV is the protocol token.", twitter: "@derivexyz", }, + nado: { + url: "https://nado.xyz", + description: + "Nado is an orderbook perpetuals DEX on Ink, the Kraken-backed L2, with a central limit orderbook and cross-margin. No token has launched yet.", + twitter: "@nadoHQ", + }, + decibel: { + url: "https://decibel.trade", + description: + "Decibel is a perpetuals DEX on Aptos with an on-chain central limit orderbook. No token has launched yet.", + twitter: "@DecibelTrade", + }, kiloex: { url: "https://kiloex.io", description: diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 97e9a0740..4c4414ca4 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -118,6 +118,9 @@ const RAW: Record = { avantis: "/logos/avantis.svg", apex: "/logos/apex.svg", derive: "/logos/derive.svg", + // Pre-TGE rows on the same bench; marks from each site, on a white tile. + nado: "/logos/nado.svg", + decibel: "/logos/decibel.svg", drift: "/logos/drift.png", vertex: "/logos/vertex.png", edgex: "/logos/edgex.jpg", diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index 760c93ce3..ba01e322e 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -255,6 +255,27 @@ export async function specToBenchmark( if (hasPanelData) r.availability = "live"; } } + // Spec-declared unranked members (provider.unranked, e.g. a perp + // DEX with no token yet on a valuation bench). They have no + // headline value by design, so leave ms at zero (every ranking + // helper skips zero rows) but carry the label so the ledger can + // list them in their own block instead of dropping them, and + // never let them read as unresponsive: the absence is intended. + const unrankedBySlug = new Map( + spec.providers + .filter((p) => p.unranked) + .map((p) => [p.slug.toLowerCase(), p.unranked as string]), + ); + if (unrankedBySlug.size > 0) { + for (const r of live.results) { + const label = unrankedBySlug.get(r.slug.toLowerCase()); + if (!label) continue; + r.unrankedLabel = label; + r.unresponsive = undefined; + r.availability = "live"; + r.ms = { p50: 0, p90: 0, p99: 0, mean: 0 }; + } + } } // Per-chain leaders/trailers: computed only on the unfiltered "All" // view of benches that declare `dimensions.chain`. Fan out one extra diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index 3dea9b58d..5c9341f2e 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -146,6 +146,18 @@ const provider = z.object({ * L1 and L2 providers (network-fees) renders an L1/L2/All toggle pill * that filters the ledger by this field. */ layer: ProviderLayer.optional(), + /** Declares a cohort member that has no headline value by design + * (a perp DEX with no token yet on a valuation bench). The ledger + * lists it in an "Unranked ·

- - View - - onSelect(null)} /> - {panels.map((p) => ( +
+
+ + View + onSelect(p.id)} - title={p.metric} + label={mainLabel} + description={mainDescription} + active={activeId == null} + onClick={() => onSelect(null)} /> - ))} + {panels.map((p) => ( + onSelect(p.id)} + /> + ))} +
+ {activeDescription && ( +

+ {activeDescription} +

+ )}
); } function Tab({ label, + description, active, onClick, - title, }: { label: string; + description?: string; active: boolean; onClick: () => void; - title?: string; }) { + const [open, setOpen] = useState(false); + const tipId = useId(); + const hasTip = !!description; + return ( - + + + {hasTip && open && ( + + + {label}:{" "} + {description} + + + )} + ); } diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index ba01e322e..f1ba968b8 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -137,6 +137,7 @@ export function buildEditorial( editorialStatus: spec.status, metric: spec.metric, panelMainLabel: spec.panel_main_label, + panelMainDescription: spec.panel_main_description, stackedShare: spec.stacked_share, unit: spec.unit, higherIsBetter: spec.higher_is_better, diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index 5c9341f2e..8ec044445 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -459,6 +459,9 @@ export const SpecSchema = z * to `metric`; set it when the panels are size/window variants and * the headline needs its variant spelled out (e.g. "All-in at $1k"). */ panel_main_label: z.string().min(1).max(80).optional(), + /* One-line definition of the headline metric, shown as the tooltip + * of the headline tab (panels carry their own `description`). */ + panel_main_description: z.string().min(1).max(300).optional(), /** When true, renders a stacked bar chart (absolute + % share) below * the main chart on the bench page. Intended for volume-share benches * where the relative contribution of each venue over time is the diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index 1dab3f107..5d126efac 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -333,6 +333,8 @@ export type Benchmark = { * `metric_panels` in the YAML. */ metricPanels?: MetricPanel[]; panelMainLabel?: string; + /** Tooltip of the headline tab in the panel switcher. */ + panelMainDescription?: string; /** When true the bench page renders a stacked bar chart (absolute + % * share over time) below the main chart. Set via `stacked_share: true` * in the bench YAML. Meaningful only for volume-share benches. */ From 16333dd2a7fd726d41165506dcf7edb35e99dbd5 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:11:22 +0200 Subject: [PATCH 7/8] bench 265 copy: meta description no longer renders "1.559xx"; intro claims matched to live floats and multiples (#2371) - seo_description and two findings appended a literal "x" after {{best_p50}} / {{worst_p50}}, which already carry the unit: the meta, OG and Twitter descriptions read "GNS trades at 1.559xx". Description also rewritten to fit the 158 char cap instead of being cut with an ellipsis. - seo_intro: HYPE/LIT/ASTER float is 23/25/35%, so "a quarter to a third of supply"; the expensive end is dYdX and Aster, with Paradex's multiple flagged as an adapter artifact (Derive sits mid-table now). - SynFutures row labelled "F (SynFutures)": the bare ticker made compare links read "GNS vs F". Claude-Session: https://claude.ai/code/session_01L4rjxemTvgRwrCG6D2iK5D Co-authored-by: Flotapponnier Co-authored-by: Claude Opus 5 (1M context) (cherry picked from commit c1231f4eb45db5547abf3b290c1abeb283bff45f) --- benchmarks/perp-pf-ratio.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/benchmarks/perp-pf-ratio.yml b/benchmarks/perp-pf-ratio.yml index 46c7e36ba..534fc08af 100644 --- a/benchmarks/perp-pf-ratio.yml +++ b/benchmarks/perp-pf-ratio.yml @@ -4,7 +4,7 @@ slug: perp-pf-ratio number: "265" title: "Perp DEX price to fees ratio 2026: Hyperliquid, Aster, Lighter, GMX, GNS live" seo_title: "Perp DEX P/F ratio 2026: Hyperliquid vs Lighter vs Aster vs GMX vs GNS" -seo_description: "{{best_name}} trades at {{best_p50}}x annualized fees, the cheapest perp DEX token. Market cap and FDV against trailing fees and revenue for {{count}} perpetual DEXes, plus the fee base of pre-TGE venues, live from DeFiLlama and CoinGecko." +seo_description: "{{best_name}} is the cheapest perp DEX token at {{best_p50}} annualized fees. P/F, FDV/F, P/S, float and open interest for {{count}} perp DEXes, live from DeFiLlama and CoinGecko." subtitle: Market cap divided by annualized trading fees (trailing 30 days) for every perp DEX with a listed token, alongside FDV, protocol revenue, float and open interest. Lower means the market pays less per dollar of fees. Same convention as the DeFiLlama P/F ranking, updated hourly. category: Trading @@ -22,12 +22,12 @@ seo_intro: | top, annualized fees at the bottom. A low P/F means the token is cheap relative to the fees its venue generates; a high P/F prices in growth, or a token that does not capture the fees. Hyperliquid, Aster and Lighter generate the most absolute fees but carry multi-billion - valuations with a quarter of supply in circulation, so their FDV/F runs far above their P/F. - GMX and Gains are fully circulating with real fee flow and trade at low single-digit - multiples. dYdX and Derive sit at the expensive end on today's fee run rate. This benchmark - tracks the whole table live from DeFiLlama (fees, revenue, open interest) and CoinGecko - (market cap, FDV, supply), the same inputs as defillama.com/pf, cut identically for every - protocol and refreshed every hour. + valuations with a quarter to a third of supply in circulation, so their FDV/F runs far above + their P/F. GMX and Gains are fully circulating with real fee flow and trade at low single-digit + multiples. dYdX and Aster sit at the expensive end on today's fee run rate; Paradex's multiple + is inflated by an incomplete DeFiLlama fee adapter. This benchmark tracks the whole table live + from DeFiLlama (fees, revenue, open interest) and CoinGecko (market cap, FDV, supply), the same + inputs as defillama.com/pf, cut identically for every protocol and refreshed every hour. abstract: | The perp-token-metrics harness polls DeFiLlama /summary/fees/[slug] for dailyFees and @@ -55,10 +55,10 @@ methodology: - "Poll cadence: DeFiLlama and CoinGecko every 60 minutes. Prometheus scrapes :2112/metrics every 60s." findings: - - "{{best_name}} is the cheapest perp DEX token at {{best_p50}}x annualized fees." + - "{{best_name}} is the cheapest perp DEX token at {{best_p50}} annualized fees." - "Hyperliquid, Aster and Lighter earn the most fees in absolute terms but trade at a large multiple, and their FDV/F is three to four times their P/F because most supply is not circulating." - "GMX and Gains are fully circulating, so their P/F and FDV/F coincide; both sit in low single digits." - - "{{worst_name}} is the most expensive token in the cohort at {{worst_p50}}x on today's fee run rate." + - "{{worst_name}} is the most expensive token in the cohort at {{worst_p50}} on today's fee run rate." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-token-metrics @@ -308,7 +308,7 @@ providers: series: perp_protocol_pf_ratio{protocol="orderly"} - slug: synfutures - name: F + name: F (SynFutures) tag: "SynFutures, parent scope: v3" formula: "P/F = CoinGecko circulating market cap / (DeFiLlama 30d fees x 365/30), parent scope" queries: From aca2679da2ac30468b62b8074dd85d4bb4845d20 Mon Sep 17 00:00:00 2001 From: Flotapponnier Date: Tue, 15 Sep 2026 01:30:22 +0200 Subject: [PATCH 8/8] bench 265: un-gate perp-pf-ratio for production (harness live on the VPS since 2026-09-14) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01L4rjxemTvgRwrCG6D2iK5D --- src/lib/removed-benches.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts index b13d16d47..ef9cd9245 100644 --- a/src/lib/removed-benches.ts +++ b/src/lib/removed-benches.ts @@ -148,12 +148,6 @@ export const REMOVED_BENCH_SLUGS = new Set([ // removal; a 3-provider bench is not strong enough signal for a // standalone page. Spec kept for carry-forward; 410 on prod. "indexing-freshness", - // perp-pf-ratio (265) held on dev until the perp-token-metrics Railway - // service is redeployed with the valuation gauges (perp_protocol_pf_*, - // mcap, float, OI). Until then the prod Prom has no series and the - // page would render an empty leaderboard. Remove here + bump the - // bench-set cache keys once the harness is live. - "perp-pf-ratio", ]); /**

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 literal 0 HcmV?d00001 diff --git a/public/logos/avantis.webp b/public/logos/avantis.webp new file mode 100644 index 0000000000000000000000000000000000000000..cc6bbe7c0371d1df951132e88954321f4c8d6345 GIT binary patch literal 1046 zcmV+x1nK)yNk&Ev1ONb6MM6+kP&iBi1ONapkHKRAub`l98%8DY__J*{4nl&&$ZN09 z|NPe>FAxX8oNK zhspWHugW|Ow(-kfm1Pu4XB2E>kX9b?s`Mc+XB@mZ0SfWlbl`YL1L zMR}FMP~>Gs!=~=VXh;-iJsf6n<_w1>Zf87nZ42XJrnJImgB;IYYvQRfa^Su8A?R=}I0GMqQ0D(R5|Tgi)4xP&8eQF=3Qd7!*yG^`J1y zat1|{RT&gURhd!Ys?4JzQ`OF(FjrL>6^)__qast3^{6lxWzMK*6ty!d3Pml93hSaO zqaryqWroF@nqCZxN=??oqEeGNqast2Iiq5xCUZu`)HRtiDl*UdvtFH1k*R6!jEYK4 zg(p97X{DxxQDHkJ*~X~IosuSJP^33c{-e@Dg=3aGB~8wtm^PmOhoprf$IBWOen6_7 zF)=l2{G(Jmbq-a^8&&?nvvw*Sr!=)roxk%a8^w-Mn#$HG^=FU#&8c^29*@U!&fl6- ztMBjD+Ix3u^|s{s`FK2rhI)O;5ByjuwQprg7-BVBdEnPddYcl4SxT=@{I-rQCyca^ zUK9RM#X4cIW%Qcx#~N0{aI07nK2X6nVZb%`gbzKxo-yPSmIO)P6N17geDulnj8Q+o z%^+zvM8$$nJhe3;Dhf1JjQqLv8JaSpqQ9UiBP!e=sZWRsH)xs>6>iX!5fyIGR1p>3 zps6A(UeNR%VUeN9Q&?1J>V~LTAgUlLc8FRL6>g&3qM{o_Wkf}Rs1;FBA<8W(cBsk- z3O7}bsK`)dQBk34MNl|oSx|J7wIV1AWLZ$GkhLNv+;mw`WXM_(6DxFCOnB0@Atom1 z$}v%)%MlU|VI?HGDYK9$5Vj#ARw&C6;YnFXL>QE<5m6v*4u}QPI^to6wlyBy#LeN* z4dPNbydZ8vG-RkN(NH1p6$};fI%1(f-}hL^7=RK9{e=;D1%ew!pd${}gdy0%pckCw z;g%@a45Ki|fKNEl!&wCMnlKKI>(^_-xgM=vzHP$k9?b39Z8hP94_0v9`nx2Y^RX6O zw7r!l&ihacE|>4E=4ZmWAE?i`KJM=OwzXDko}UkBJc_^PgxmkO|8M`_{=fZy`~UX; Q?f={VxBqYd-~Rs(VSK;)C;$Ke literal 0 HcmV?d00001 diff --git a/public/logos/derive.webp b/public/logos/derive.webp new file mode 100644 index 0000000000000000000000000000000000000000..c04facbbe770cd5ea7164e0d948efa488a7499ef GIT binary patch literal 37074 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ⅈlR` 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 = { "SynFutures is an oAMM-based (Oyster AMM) perpetuals DEX on Base and Arbitrum. Traders open positions against a concentrated liquidity pool rather than a matching engine; any ERC-20 can be listed permissionlessly.", twitter: "@SynFuturesDefi", }, + // ─── Perp DEX valuation (bench 265 perp-pf-ratio) ───────────── + avantis: { + url: "https://avantisfi.com", + description: + "Avantis is a synthetic perpetuals DEX on Base covering crypto, forex and commodities. Trades settle against a USDC vault with oracle pricing; AVNT is the protocol token.", + twitter: "@avantisfi", + }, + apex: { + url: "https://apex.exchange", + description: + "ApeX is an orderbook perpetuals exchange (ApeX Omni, formerly ApeX Pro) with omnichain deposits and off-chain matching. APEX is the protocol token.", + twitter: "@OfficialApeXdex", + }, + derive: { + url: "https://derive.xyz", + description: + "Derive (formerly Lyra) is an options and perpetuals exchange on its own OP Stack rollup with an off-chain orderbook and on-chain settlement. DRV is the protocol token.", + twitter: "@derivexyz", + }, kiloex: { url: "https://kiloex.io", description: diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index ec94aba15..af4ff95e2 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -114,6 +114,10 @@ const RAW: Record = { kiloex: "/logos/kiloex.png", orderly: "/logos/orderly.webp", backpack: "/logos/backpack.webp", + // Perp DEX valuation bench (perp-pf-ratio); icons from DeFiLlama. + avantis: "/logos/avantis.webp", + apex: "/logos/apex.webp", + derive: "/logos/derive.webp", drift: "/logos/drift.png", vertex: "/logos/vertex.png", edgex: "/logos/edgex.jpg", diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts index ef9cd9245..b13d16d47 100644 --- a/src/lib/removed-benches.ts +++ b/src/lib/removed-benches.ts @@ -148,6 +148,12 @@ export const REMOVED_BENCH_SLUGS = new Set([ // removal; a 3-provider bench is not strong enough signal for a // standalone page. Spec kept for carry-forward; 410 on prod. "indexing-freshness", + // perp-pf-ratio (265) held on dev until the perp-token-metrics Railway + // service is redeployed with the valuation gauges (perp_protocol_pf_*, + // mcap, float, OI). Until then the prod Prom has no series and the + // page would render an empty leaderboard. Remove here + bump the + // bench-set cache keys once the harness is live. + "perp-pf-ratio", ]); /** diff --git a/src/lib/spec.ts b/src/lib/spec.ts index dfb04e7d5..1d9fe6d19 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -391,7 +391,8 @@ const loadBenchmarkUnfilteredCached = unstable_cache( // v55: add ws-head-latency-robinhood (244) + keyed-rpc-robinhood (243). Bench SET grew. // v56: drop 6 keyed-rpc benches (arbitrum/base/bnb/eth/polygon/solana), US-only robinhood. // v57: drop rpc-keyed-latency bench. - ["bench-unfiltered-v62", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + // v63: add bench 265 perp-pf-ratio + 5 providers on bench 234. Bench SET grew. + ["bench-unfiltered-v63", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); @@ -604,7 +605,8 @@ const loadAllBenchmarksCached = unstable_cache( // v52: token-quote-coverage flipped draft→live (see bench-unfiltered-v49). // v53: lockstep with bench-unfiltered-v53 (Flashbots prune). // v57: lockstep with bench-unfiltered-v54 (add 6 new chain benches 216-221). - ["all-benchmarks-v57", process.env.VERCEL_ENV === "production" ? "prod" : "all"], + // v58: lockstep with bench-unfiltered-v63 (add bench 265 perp-pf-ratio). + ["all-benchmarks-v58", process.env.VERCEL_ENV === "production" ? "prod" : "all"], { revalidate: 300, tags: ["benchmarks"] }, ); export const loadAllBenchmarks = cache(loadAllBenchmarksCached); diff --git a/src/lib/stats.test.ts b/src/lib/stats.test.ts new file mode 100644 index 000000000..9203b643a --- /dev/null +++ b/src/lib/stats.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from "bun:test"; +import { computeFieldStats } from "./stats"; +import type { ProviderResult } from "@/types/benchmark"; + +function row(slug: string, p50: number, availability: ProviderResult["availability"] = "live"): ProviderResult { + return { + slug, + name: slug, + ms: { p50, p90: p50, p99: p50, mean: p50 }, + successRate: 100, + availability, + } as ProviderResult; +} + +describe("computeFieldStats", () => { + test("ignores unavailable rows", () => { + const s = computeFieldStats([row("a", 5), row("b", 9), row("dead", 0, "unavailable")]); + expect(s.fieldMin).toBe(5); + expect(s.fieldMax).toBe(9); + }); + + test("ignores live rows with no headline value (all-zero), like the ledger", () => { + // A token-less venue on a valuation bench: promoted to live by its + // panel data, but no P/F to rank. Best must not read 0. + const s = computeFieldStats([row("gains", 1.6), row("gmx", 3.4), row("ostium", 0)]); + expect(s.fieldMin).toBe(1.6); + expect(s.fieldMedian).toBe(3.4); + expect(s.fieldMax).toBe(3.4); + }); + + test("keeps negative rows on signed benches", () => { + const s = computeFieldStats([row("okx", -3), row("bybit", 2)]); + expect(s.fieldMin).toBe(-3); + expect(s.fieldMax).toBe(2); + }); + + test("empty field yields zeros", () => { + const s = computeFieldStats([row("x", 0)]); + expect(s).toEqual({ fieldMin: 0, fieldMedian: 0, fieldMax: 0, tailMin: 0, tailMax: 0, tailSpread: 0 }); + }); +}); diff --git a/src/lib/stats.ts b/src/lib/stats.ts index 5bbf2bfe9..65e66df8e 100644 --- a/src/lib/stats.ts +++ b/src/lib/stats.ts @@ -18,7 +18,17 @@ export function computeFieldStats(results: ProviderResult[]): { // placeholder values are 0 and would otherwise drag the Best stat to // 0 ms, the Spread tailMin to 0 (which kills the ratio), and the // Median toward the lower half of the field. - const live = results.filter((r) => r.availability !== "unavailable"); + // Also drop all-zero rows: the load path promotes a provider with + // companion-panel data but no headline value to "live" with p50=0 + // (a token-less venue on a valuation bench), and the ledger already + // prunes such rows, so the strip must not report Best = 0 for a + // provider the table does not rank. Signed benches keep negative + // rows because the check is "not all zero", not "> 0". + const live = results.filter( + (r) => + r.availability !== "unavailable" && + (r.ms.p50 !== 0 || r.ms.p90 !== 0 || r.ms.p99 !== 0), + ); const p50s = live.map((r) => r.ms.p50); const p99s = live.map((r) => r.ms.p99); From a9652255832a33ccb2a5156b1e22fed1f9fdaae6 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:44:21 +0200 Subject: [PATCH 2/8] logos: official SVG marks for Avantis, ApeX and Derive (#2341) Avantis: the "A" glyph extracted from the wordmark SVG served on avantisfi.com, set on the brand purple square like their app icon. ApeX: the ape mark from apex.exchange's header logo SVG, squared. Derive: the horse mark SVG served on derive.xyz, as is. Claude-Session: https://claude.ai/code/session_01L4rjxemTvgRwrCG6D2iK5D Co-authored-by: Flotapponnier Co-authored-by: Claude Opus 5 (1M context) (cherry picked from commit d5b599a8f5392d4dce6eecdce3798b75c8070034) --- public/logos/apex.svg | 4 ++++ public/logos/apex.webp | Bin 29302 -> 0 bytes public/logos/avantis.svg | 7 +++++++ public/logos/avantis.webp | Bin 1046 -> 0 bytes public/logos/derive.svg | 1 + public/logos/derive.webp | Bin 37074 -> 0 bytes src/lib/logo-manifest.ts | 8 ++++---- 7 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 public/logos/apex.svg delete mode 100644 public/logos/apex.webp create mode 100644 public/logos/avantis.svg delete mode 100644 public/logos/avantis.webp create mode 100644 public/logos/derive.svg delete mode 100644 public/logos/derive.webp diff --git a/public/logos/apex.svg b/public/logos/apex.svg new file mode 100644 index 000000000..88a02e910 --- /dev/null +++ b/public/logos/apex.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/logos/apex.webp b/public/logos/apex.webp deleted file mode 100644 index af2bd8c7ebf5cb2a2da924b142e6a6695979cf62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29302 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