From 2751ee4c1f49a8af0b6fd1e5252dd7f133892a41 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Sun, 26 Jul 2026 07:58:14 +0200 Subject: [PATCH 1/3] gas bench: register MetaMask oracle in config (const + interval + endpoint + SupportedSet) --- harnesses/gas-estimation/cmd/script/config.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/harnesses/gas-estimation/cmd/script/config.go b/harnesses/gas-estimation/cmd/script/config.go index bfc4ccda..f119551d 100644 --- a/harnesses/gas-estimation/cmd/script/config.go +++ b/harnesses/gas-estimation/cmd/script/config.go @@ -32,6 +32,7 @@ const ( OraclePublicNode Oracle = "publicnode-feehistory" OracleOwlracle Oracle = "owlracle" OracleEtherscan Oracle = "etherscan" + OracleMetaMask Oracle = "metamask" ) type Oracle string @@ -48,6 +49,7 @@ var pollIntervals = map[Oracle]time.Duration{ OraclePublicNode: 12 * time.Second, OracleOwlracle: 60 * time.Second, OracleEtherscan: 15 * time.Second, + OracleMetaMask: 12 * time.Second, } // Realized-block poll cadence per chain. Picked close to each chain's @@ -94,7 +96,7 @@ func chains() []Chain { OwlracleSlug: "eth", BlockTimeSec: 12, // Etherscan v2 free tier covers chainid=1. All four oracles work. - SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan}, + SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan, OracleMetaMask}, }, { Slug: "polygon", @@ -104,7 +106,7 @@ func chains() []Chain { OwlracleSlug: "poly", BlockTimeSec: 2, // Etherscan v2 free tier covers chainid=137 (verified). All four oracles work. - SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan}, + SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan, OracleMetaMask}, }, { Slug: "avalanche", @@ -114,8 +116,8 @@ func chains() []Chain { OwlracleSlug: "avax", BlockTimeSec: 2, // Etherscan v2 returns "Free API access is not supported for this chain" on chainid=43114 — paid plan required. - // Three oracles only (Blocknative + PublicNode feeHistory + Owlracle). - SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle}, + // Three oracles only (PublicNode feeHistory + Owlracle + MetaMask). + SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleMetaMask}, }, } } @@ -161,6 +163,10 @@ func endpointForChain(o Oracle, c Chain) OracleEndpoint { return OracleEndpoint{ URL: fmt.Sprintf("https://api.etherscan.io/v2/api?chainid=%d&module=gastracker&action=gasoracle", c.ChainID), } + case OracleMetaMask: + return OracleEndpoint{ + URL: fmt.Sprintf("https://gas.api.cx.metamask.io/networks/%d/suggestedGasFees", c.ChainID), + } } return OracleEndpoint{} } From 5a95502f3d28cf7f57429b77aca852863a293c13 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Sun, 26 Jul 2026 07:58:43 +0200 Subject: [PATCH 2/3] =?UTF-8?q?gas=20bench:=20pollMetaMask=20(EIP-1559=20l?= =?UTF-8?q?ow/medium/high=20=E2=86=92=20p25/p50/p90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gas-estimation/cmd/script/oracles.go | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/harnesses/gas-estimation/cmd/script/oracles.go b/harnesses/gas-estimation/cmd/script/oracles.go index a630cc58..d733a1b3 100644 --- a/harnesses/gas-estimation/cmd/script/oracles.go +++ b/harnesses/gas-estimation/cmd/script/oracles.go @@ -50,6 +50,8 @@ func pollOracle(ctx context.Context, o Oracle, ep OracleEndpoint) pollResult { return pollOwlracle(ctx, ep) case OracleEtherscan: return pollEtherscan(ctx, ep) + case OracleMetaMask: + return pollMetaMask(ctx, ep) default: return pollResult{Err: fmt.Errorf("unknown oracle: %s", o)} } @@ -401,6 +403,77 @@ func pollEtherscan(ctx context.Context, ep OracleEndpoint) pollResult { } } +// ─── MetaMask (gas.api.cx.metamask.io) ──────────────────────────── +// +// Public no-key endpoint that powers the MetaMask wallet's +// suggested-fee UI at real production scale. EIP-1559 native: +// returns explicit low/medium/high tiers each with their own +// `suggestedMaxPriorityFeePerGas` and `suggestedMaxFeePerGas` +// (gwei decimal strings), plus a baseline `estimatedBaseFee`. +// We map low→p25, medium→p50, high→p90 — the same mapping the +// wallet's own slow/market/fast selector uses. Tiers collapse +// during calm blocks (medium and high often equal), which +// mirrors the upstream model's documented behaviour; the bench +// surfaces this as-is rather than masking it. + +type mmTier struct { + SuggestedMaxPriorityFeePerGas string `json:"suggestedMaxPriorityFeePerGas"` + SuggestedMaxFeePerGas string `json:"suggestedMaxFeePerGas"` +} + +type mmResp struct { + Low mmTier `json:"low"` + Medium mmTier `json:"medium"` + High mmTier `json:"high"` + EstimatedBaseFee string `json:"estimatedBaseFee"` +} + +func pollMetaMask(ctx context.Context, ep OracleEndpoint) pollResult { + req, _ := http.NewRequestWithContext(ctx, "GET", ep.URL, nil) + body, status, err := httpDo(ctx, req) + if err != nil { + return pollResult{Err: err} + } + if status == 429 { + return pollResult{Err: fmt.Errorf("throttled (HTTP 429)")} + } + if status != 200 { + return pollResult{Err: fmt.Errorf("http %d", status)} + } + var r mmResp + if err := json.Unmarshal(body, &r); err != nil { + return pollResult{Err: fmt.Errorf("parse: %w", err)} + } + base, err := strconv.ParseFloat(r.EstimatedBaseFee, 64) + if err != nil { + return pollResult{Err: fmt.Errorf("baseFee parse: %w", err)} + } + parseTier := func(field, name string) (float64, error) { + v, err := strconv.ParseFloat(field, 64) + if err != nil { + return 0, fmt.Errorf("%s parse: %w", name, err) + } + return v, nil + } + low, e1 := parseTier(r.Low.SuggestedMaxPriorityFeePerGas, "low") + med, e2 := parseTier(r.Medium.SuggestedMaxPriorityFeePerGas, "medium") + high, e3 := parseTier(r.High.SuggestedMaxPriorityFeePerGas, "high") + if e1 != nil || e2 != nil || e3 != nil { + return pollResult{Err: fmt.Errorf("tier parse: %v / %v / %v", e1, e2, e3)} + } + // No explicit target block: the API applies to "the next block" + // but never names it. Realizer grafts head+1 the same way it + // does for Owlracle. + return pollResult{ + BaseGwei: base, + Predictions: []Prediction{ + {Oracle: OracleMetaMask, Tier: TierP25, PriorityGwei: low, BaseGwei: base}, + {Oracle: OracleMetaMask, Tier: TierP50, PriorityGwei: med, BaseGwei: base}, + {Oracle: OracleMetaMask, Tier: TierP90, PriorityGwei: high, BaseGwei: base}, + }, + } +} + // ─── helpers ─────────────────────────────────────────────────────── func httpDo(ctx context.Context, req *http.Request) ([]byte, int, error) { From 8525d975a326849f7bce65d1641715e597562512 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Sun, 26 Jul 2026 07:59:45 +0200 Subject: [PATCH 3/3] gas bench: add MetaMask provider block + methodology mentions --- benchmarks/gas-estimation.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/benchmarks/gas-estimation.yml b/benchmarks/gas-estimation.yml index 2cab18ff..e61ee54c 100644 --- a/benchmarks/gas-estimation.yml +++ b/benchmarks/gas-estimation.yml @@ -82,9 +82,9 @@ abstract: | methodology: - "Chains: Ethereum mainnet (chainid=1), Polygon PoS (chainid=137). Both are EIP-1559 with proper dynamic base fee, so priority-fee prediction is apples-to-apples comparable. BNB Chain excluded (not EIP-1559). Avalanche C-Chain excluded (auto-tuning drives priority fee to ~0, prediction-error collapses to ~0)." - - "Oracles per chain. Ethereum + Polygon: PublicNode feeHistory, Owlracle, Etherscan v2 (free tier)." - - "Per-oracle endpoints. PublicNode `eth_feeHistory` on each chain's RPC; Owlracle `/v4/{eth|poly}/gas` (slug per chain); Etherscan v2 `?chainid=&module=gastracker&action=gasoracle`." - - "Cadences. PublicNode feeHistory every 12s per chain; Owlracle every 60s per chain (free quota ceiling); Etherscan every 15s per chain with a global rate-gate enforcing ≥6s between any two Etherscan requests across chains (no-key limit is 1 req/5s per IP, shared)." + - "Oracles per chain. Ethereum + Polygon: PublicNode feeHistory, Owlracle, Etherscan v2 (free tier), MetaMask. Avalanche: same set minus Etherscan (free plan restricted for chainid 43114)." + - "Per-oracle endpoints. PublicNode `eth_feeHistory` on each chain's RPC; Owlracle `/v4/{eth|poly|avax}/gas` (slug per chain); Etherscan v2 `?chainid=&module=gastracker&action=gasoracle`; MetaMask `gas.api.cx.metamask.io/networks//suggestedGasFees` (public no-key, EIP-1559 low/medium/high mapped to p25/p50/p90)." + - "Cadences. PublicNode feeHistory and MetaMask every 12s per chain; Owlracle every 60s per chain (free quota ceiling); Etherscan every 15s per chain with a global rate-gate enforcing ≥6s between any two Etherscan requests across chains (no-key limit is 1 req/5s per IP, shared)." - "Tier normalization. Each oracle's named tiers (fast / standard / slow / safe / propose) are mapped onto a unified p25 / p50 / p75 / p90 / p99 scheme. The leaderboard ranks the p50 tier (the standard speed wallets use by default)." - "Realized priority fee. For each pending block, the harness pulls the full block via `eth_getBlockByNumber(.., true)` on the chain's primary RPC and computes p25/p50/p75/p90/p99 directly from the actual effective `maxPriorityFeePerGas` values across every included transaction. Every tier is a real percentile of the mined block, none are interpolated. Empty or low-tx blocks (`gas_realized_tx_count` near zero) are surfaced separately because the realized percentile is noisy when tx count is low." - "Realized-side independence check. Each chain configures a second RPC on a different upstream (dRPC / native chain RPC) as `GAS_REALIZED_RPC_VERIFY_`; on each block the harness cross-checks baseFee and increments `gas_realized_quorum_disagreement_total{chain, kind}` on mismatch. Primary is still trusted; the counter makes visible whether our ground truth is actually independent across upstreams." @@ -217,3 +217,16 @@ providers: success: sum(rate(gas_oracle_call_total{oracle="etherscan", result="ok"}[24h])) / sum(rate(gas_oracle_call_total{oracle="etherscan"}[24h])) sample_size: sum(increase(gas_oracle_call_total{oracle="etherscan"}[24h])) series: gas_error_priority_gwei{oracle="etherscan", tier="p50"} + + - slug: metamask + name: MetaMask + tag: Wallet-native, no key, EIP-1559 low/medium/high + formula: "p99 over 24h of |MetaMask's medium-tier suggestedMaxPriorityFeePerGas mapped to p50 − realized p50 priority fee| in gwei, polled every 12s and matched to the next-mined block on the active chain." + queries: + p50: quantile_over_time(0.99, gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h]) + p90: quantile_over_time(0.90, gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h]) + p99: quantile_over_time(0.50, gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h]) + mean: avg_over_time(gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h]) + success: sum(rate(gas_oracle_call_total{oracle="metamask", result="ok"}[24h])) / sum(rate(gas_oracle_call_total{oracle="metamask"}[24h])) + sample_size: sum(increase(gas_oracle_call_total{oracle="metamask"}[24h])) + series: gas_error_priority_gwei{oracle="metamask", tier="p50"}