Skip to content

Add native-token USD prices and execution-client fingerprinting#40

Merged
Johnaverse merged 4 commits into
mainfrom
feat/native-prices-and-clients
May 14, 2026
Merged

Add native-token USD prices and execution-client fingerprinting#40
Johnaverse merged 4 commits into
mainfrom
feat/native-prices-and-clients

Conversation

@Johnaverse
Copy link
Copy Markdown
Owner

Summary

  • Native-token prices on /chains, /chains/:id, and the equivalent MCP tools. Sourced from CoinGecko via a new priceService that keys cache by coinId (sibling chains share entries), coalesces concurrent fetches, applies a 3s timeout, negatively caches missing IDs, and prefetches on startup so cold /chains requests don't block.
  • Execution-client fingerprinting: web3_clientVersion is parsed into { name, version, repo, language, layer } (see clientParser.js + clientRegistry.js). New /clients and /clients/:id REST endpoints and a get_clients MCP tool expose per-chain summaries with node-count and version breakdowns. /rpc-monitor/:id also gains a clients array.
  • New env knobs: PRICE_CACHE_TTL_MS, PRICE_NEGATIVE_CACHE_TTL_MS, PRICE_FETCH_TIMEOUT_MS.
  • Small refactor of public/app.js (extract-method on showNodeDetails).

Test plan

  • npm test — 549 passed, 4 skipped
  • Hit /chains on a cold cache and confirm the response includes price for known chains (Ethereum, Polygon, BSC, ...) and null for unknown ones
  • Hit /chains/:id for chainId 1 and confirm price.usd is a number
  • Hit /clients after the RPC monitor has run at least once; confirm chains with parsed web3_clientVersion show client summaries
  • Hit /clients/1 and confirm clients[].versions is sorted by nodeCount descending
  • Confirm CoinGecko failure (e.g. set PRICE_FETCH_TIMEOUT_MS=1) degrades gracefully — price: null rather than 5xx
  • Sanity-check the MCP get_clients tool from a connected client

Two new product features sharing the existing RPC monitor pipeline:

- Native-token prices: `/chains` and `/chains/:id` now include a `price: { usd, updatedAt } | null` field sourced from CoinGecko. New `priceService.js` keys its cache by CoinGecko coinId (so sibling chains like all ETH-L2s share a single entry), coalesces concurrent fetches, applies a 3s timeout per request, negatively caches missing IDs with a short TTL, and is prefetched on startup so the first `/chains` call doesn't pay a round-trip on the hot path.
- Client diversity: each working RPC endpoint's `web3_clientVersion` is parsed into structured metadata (name, version, repo, language, layer). New `/clients` and `/clients/:id` endpoints expose per-chain client summaries with node counts and version breakdowns. `/rpc-monitor/:id` also gains a `clients` array.

Both surfaces are exposed as MCP tools (`get_clients`) and the get_chains/get_chain_by_id MCP responses are price-enriched. Adds env vars PRICE_CACHE_TTL_MS, PRICE_NEGATIVE_CACHE_TTL_MS, PRICE_FETCH_TIMEOUT_MS. Includes unit + integration tests covering parser edge cases, cache/coalescing behavior, and the new routes.
Copilot AI review requested due to automatic review settings May 12, 2026 12:49
Comment thread tests/unit/rpcMonitor.test.js Fixed
Comment thread tests/unit/rpcMonitor.test.js Fixed
…ring sanitization'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds native-token USD pricing (via CoinGecko + caching) to chain responses and introduces execution-client fingerprinting based on web3_clientVersion, exposing aggregated client summaries via new REST + MCP endpoints.

Changes:

  • Add price to /chains, /chains/:id, and MCP chain tools via new priceService (TTL cache, negative cache, inflight coalescing, timeout, startup prefetch).
  • Parse web3_clientVersion into structured client metadata and aggregate per-chain client/version counts; expose via /clients, /clients/:id, get_clients, and include in /rpc-monitor/:id.
  • Small UI refactor in public/app.js (extract helper methods from showNodeDetails).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/rpcMonitor.test.js Adds unit coverage for parsed client field and per-chain client aggregation helpers.
tests/unit/priceService.test.js New unit tests for CoinGecko ID mapping, caching behavior, and batch fetch logic.
tests/unit/mcp-tools.test.js Updates MCP tool mocks and assertions for new price enrichment and get_clients.
tests/unit/clientParser.test.js New unit tests for web3_clientVersion parsing across known/unknown clients and edge cases.
tests/integration/api.test.js Extends API integration tests to assert price fields and new /clients endpoints.
rpcMonitor.js Adds client parsing to endpoint results plus aggregation helpers (getClientsByChain, summarizeChainClients).
public/app.js Extracts badge/website rendering helpers from showNodeDetails.
priceService.js Implements CoinGecko pricing fetch + caching + prefetch.
mcp-tools.js Enriches chain tools with price and adds a new get_clients MCP tool.
index.js Adds price enrichment to chain endpoints, adds /clients REST endpoints, and includes client summaries in /rpc-monitor/:id.
config.js Adds env-configured TTLs and timeout for price caching/fetching.
clientRegistry.js Introduces a registry mapping client names to metadata (repo/language/website/layer).
clientParser.js Adds web3_clientVersion parser that normalizes and enriches client metadata via registry lookups.

Comment thread priceService.js Outdated
Comment on lines +143 to +147
} else {
// Negative cache: short TTL so we don't hammer CoinGecko on every request
// when an ID is missing or temporarily unavailable.
priceCache.set(id, { usd: null, updatedAt });
}
Comment thread clientRegistry.js Outdated
Comment on lines +1 to +4
// Curated registry of known blockchain client software.
// Key is the lowercased, hyphen-free client name as it appears in
// `web3_clientVersion` responses. Add new entries as they're observed in
// production — unknown names still round-trip through the parser with `repo: null`.
Comment thread clientParser.js Outdated
Comment on lines +73 to +75
* Extract the semver portion of a version segment, preserving an optional `v`
* prefix and pre-release/build metadata. Some clients emit extra descriptors
* (e.g. "v1.26.0+commit.abc"); we keep them intact — the raw string is still
Comment thread rpcMonitor.js Outdated
Comment on lines +243 to +244
* array of summaries — one per chain with at least one working endpoint that
* reported a parseable `client`.
claude added 2 commits May 13, 2026 23:59
Resolves conflicts by favoring main's RPC-monitoring structure
(dataService.js owns monitoring; rpcMonitor.js stays as legacy).
New client-aggregation functions moved out of rpcMonitor.js into
a dedicated clientsView.js that consumes getRpcMonitoringResults
from dataService.js and parses clientVersion via clientParser.js.

- index.js, mcp-tools.js: import RPC monitor fns from dataService.js,
  import getClientsByChain/summarizeChainClients from clientsView.js,
  keep priceService imports
- public/app.js: take main's showNodeHeader extraction; drop the PR's
  duplicate showStatusBadge / inlined helpers superseded by main's
  refactor; keep the CodeQL URL-protocol fix
- rpcMonitor.js, tests/unit/rpcMonitor.test.js: restored to main's
  version (PR's additions superseded by clientsView.js)
- clientsView.js, tests/unit/clientsView.test.js: new
- tests/integration/api.test.js, tests/unit/mcp-tools.test.js:
  switch mocks from rpcMonitor.js to clientsView.js

All 3 remaining test failures (dataService loadData "all sources
failing", mcp-tools get_stats x2) are pre-existing on main.
priceService: distinguish "upstream failed" from "id missing". On HTTP /
network error the cache is no longer poisoned with negative entries —
retries can immediately re-fetch instead of waiting for
PRICE_NEGATIVE_CACHE_TTL_MS. Two new tests cover the retry path after
both network and HTTP failures.

clientRegistry: drop misleading "hyphen-free" claim from the header
comment — registry already contains op-geth and op-reth.

clientParser: rewrite normalizeVersion docstring to describe what the
function actually does (trim) rather than implying semver extraction.

clientsView: getClientsByChain() (no-arg form) now filters out chains
where every working endpoint had an unparseable client, so /clients
stays a directory of known software. Single-chain form unchanged so
callers can still see unknownNodes.
Copilot AI review requested due to automatic review settings May 14, 2026 00:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 11 comments.

Comment thread priceService.js
// Negative-cache with the short TTL so we don't hammer CoinGecko.
priceCache.set(id, { usd: null, updatedAt });
}
// else: upstream failed; leave any prior entry intact so retries can succeed.
Comment thread mcp-tools.js
Comment on lines +18 to 19
import { getPricesForChains, getPriceForChain } from './priceService.js';

Comment thread config.js
Comment on lines +85 to +87
export const PRICE_CACHE_TTL_MS = parseIntEnv('PRICE_CACHE_TTL_MS', 3600000);
export const PRICE_NEGATIVE_CACHE_TTL_MS = parseIntEnv('PRICE_NEGATIVE_CACHE_TTL_MS', 300000);
export const PRICE_FETCH_TIMEOUT_MS = parseIntEnv('PRICE_FETCH_TIMEOUT_MS', 3000);
Comment thread priceService.js
Comment on lines +90 to +93
for (const id of coinIds) {
const pending = inflight.get(id);
if (pending) {
waiters.push(pending.then(({ map, ok: pendingOk }) => ({ id, usd: map.get(id), ok: pendingOk })));
Comment thread priceService.js
Comment on lines +149 to +152
} else if (ok) {
// Upstream succeeded but didn't return this id — it's genuinely missing.
// Negative-cache with the short TTL so we don't hammer CoinGecko.
priceCache.set(id, { usd: null, updatedAt });
Comment thread mcp-tools.js
}

const { chainId } = args;
if (!isValidChainId(chainId)) {
Comment thread mcp-tools.js
return { content: [{ type: 'text', text: lines.join('\n') }] };
}

function handleGetClients(args) {
Comment thread priceService.js
Comment on lines +105 to +107
const response = await fetchWithTimeout(url);
if (response.ok) {
const data = await response.json();
Comment thread clientsView.js
Comment on lines +15 to +16
* endpoint failed to parse are excluded from the list so `/clients`
* stays a directory of *known* client software.
Comment thread mcp-tools.js
Comment on lines +222 to +223
const chainIds = chains.map((c) => c.chainId);
const priceMap = await getPricesForChains(chainIds);
@Johnaverse Johnaverse merged commit 0d93056 into main May 14, 2026
8 of 9 checks passed
@Johnaverse Johnaverse deleted the feat/native-prices-and-clients branch May 14, 2026 01:04
Johnaverse pushed a commit that referenced this pull request May 14, 2026
Brings in PR #40 (native-token USD prices + execution-client
fingerprinting) and PR #41 (dependabot fastify 5.8.5 bump).

Conflict resolution favored PR #39's architectural structure: the
PR #40 features are ported into the new src/ layout instead of into
the old monolithic index.js / mcp-tools.js.

- src/http/routes/chains.js: /chains and /chains/:id now enrich
  responses with price via priceService.
- src/http/routes/clients.js (new): /clients and /clients/:id
  expose getClientsByChain output.
- src/http/routes/rpcMonitor.js: /rpc-monitor/:id gains a clients
  array via summarizeChainClients.
- src/http/app.js: registers clientsRoutes, kicks off prefetchAllPrices
  on startup (with pino-logged failure).
- index.js: kept the PR #39 shim form (imports buildApp from
  src/http/app.js).
- mcp-tools.js: keeps PR #39's L2BEAT/refresher tools (get_scaling_chains,
  get_l2beat_by_id, get_refresher_status) AND adds PR #40's get_clients;
  get_chains / get_chain_by_id handlers enriched with price.
- package.json: union of deps — pino ^10.3.0 (PR #39) + fastify
  ^5.8.5 (PR #41).
- tests/integration/api.test.js: kept the PR #39 hoisted-mocks pattern,
  added priceService and clientsView mock factories so the new routes
  resolve under mocked seams.
- tests/unit/mcp-tools.test.js: kept l2beatRefresher mock, added
  clientsView + priceService mocks; tool count is now 17 (13 base + 3
  L2BEAT/refresher + 1 clients).
- package-lock.json: regenerated by npm install.

Test result: 670 passing / 0 failing / 4 skipped (PR #39 was 618/0/4
and PR #40 added priceService + clientsView + clientParser test
modules; merged total adds 52 net new passing tests).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants