> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whitechain.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Gas and network tracker example

> Track live gas tiers, coin price, fees, and network stats on Whitechain Sepolia with the Blockscout Etherscan-compatible RPC API and REST v2 stats, with copy-paste requests and real responses.

export const ApiRunner = ({method = "GET", url, body, inputLabel, inputValue = "", inputPlaceholder, placeholder = "{input}", path, pathLabel = "Result", format, decimals, unit, runLabel = "Run this request"}) => {
  const [field, setField] = useState(inputValue);
  const [status, setStatus] = useState("idle");
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);
  const getPath = (obj, dotted) => {
    if (!dotted) return undefined;
    return dotted.split(".").reduce((acc, key) => acc === null || acc === undefined ? acc : acc[key], obj);
  };
  const toBigInt = value => {
    try {
      const text = String(value).trim();
      if (text.startsWith("0x") || text.startsWith("0X")) return BigInt(text);
      return BigInt(text.split(".")[0]);
    } catch (err) {
      return null;
    }
  };
  const formatUnits = (amount, places) => {
    if (places <= 0) return amount.toString();
    const scale = BigInt(10) ** BigInt(places);
    const whole = (amount / scale).toString();
    const fraction = (amount % scale).toString().padStart(places, "0").replace(/0+$/, "");
    return fraction ? whole + "." + fraction : whole;
  };
  const formatValue = raw => {
    const suffix = unit ? " " + unit : "";
    if (format === "hex") {
      const parsed = toBigInt(raw);
      return parsed === null ? String(raw) : parsed.toString() + suffix;
    }
    if (format === "wei") {
      const parsed = toBigInt(raw);
      return parsed === null ? String(raw) : formatUnits(parsed, 18) + suffix;
    }
    if (format === "units") {
      const parsed = toBigInt(raw);
      return parsed === null ? String(raw) : formatUnits(parsed, Number(decimals || 0)) + suffix;
    }
    return String(raw) + suffix;
  };
  const substitute = text => {
    if (!text || !inputLabel) return text;
    return text.split(placeholder).join(field.trim());
  };
  const target = substitute(url);
  const payload = substitute(body);
  const run = async () => {
    setStatus("loading");
    setError(null);
    setResult(null);
    try {
      const response = await fetch(target, {
        method,
        headers: method === "POST" ? {
          "content-type": "application/json"
        } : {
          accept: "application/json"
        },
        body: method === "POST" ? payload : undefined
      });
      const text = await response.text();
      let parsed = null;
      try {
        parsed = JSON.parse(text);
      } catch (err) {
        parsed = null;
      }
      if (!response.ok) {
        setError("HTTP " + response.status + ". " + text.slice(0, 300));
        setStatus("error");
        return;
      }
      if (parsed === null) {
        setError("The response was not JSON: " + text.slice(0, 300));
        setStatus("error");
        return;
      }
      if (parsed.error) {
        setError("JSON-RPC error: " + JSON.stringify(parsed.error));
        setStatus("error");
        return;
      }
      if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
        setError("GraphQL error: " + (parsed.errors[0].message || "query rejected"));
        setStatus("error");
        return;
      }
      if (parsed.status === "0") {
        setError("API error: " + (parsed.message || "request failed"));
        setStatus("error");
        return;
      }
      setResult(parsed);
      setStatus("done");
    } catch (err) {
      setError("Request failed: " + String(err.message || err) + ". Check your network, or a browser extension blocking the request.");
      setStatus("error");
    }
  };
  const readHeadline = () => {
    if (!path) return null;
    try {
      const raw = getPath(result, path);
      if (raw === undefined) return "not present in the response";
      if (raw === null) return "null";
      if (typeof raw === "object") return JSON.stringify(raw);
      return formatValue(raw);
    } catch (err) {
      return "could not be read: " + String(err.message || err);
    }
  };
  return <div className="not-prose my-4 rounded-xl border border-gray-200 bg-gray-50/70 p-4 dark:border-white/10 dark:bg-white/5">
      <div className="flex flex-wrap items-end gap-3">
        {inputLabel ? <label className="flex min-w-[18rem] grow flex-col gap-1 text-xs font-medium text-gray-600 dark:text-gray-300">
            {inputLabel}
            <input type="text" value={field} spellCheck={false} autoComplete="off" placeholder={inputPlaceholder} onChange={event => setField(event.target.value)} className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 font-mono text-xs text-gray-900 dark:border-white/10 dark:bg-black/30 dark:text-gray-100" />
          </label> : null}
        <button type="button" onClick={run} disabled={status === "loading"} className="rounded-lg bg-[#0066FF] px-4 py-2 text-sm font-medium text-white disabled:opacity-60">
          {status === "loading" ? "Running" : runLabel}
        </button>
      </div>

      <p className="mt-2 break-all font-mono text-[11px] text-gray-500 dark:text-gray-400">
        {method} {target}
      </p>

      <div aria-live="polite">
        {status === "error" ? <p className="mt-3 text-sm text-red-600 dark:text-red-400">{error}</p> : null}
        {status === "done" ? <div className="mt-3">
            {path ? <p className="text-sm text-gray-700 dark:text-gray-200">
                <span className="font-medium">{pathLabel}: </span>
                <span className="font-mono">{readHeadline()}</span>
              </p> : null}
            <pre className="mt-2 max-h-72 overflow-auto rounded-lg bg-white p-3 text-xs leading-relaxed text-gray-800 dark:bg-black/40 dark:text-gray-200">
              {JSON.stringify(result, null, 2)}
            </pre>
          </div> : null}
      </div>
    </div>;
};

<Note>
  All requests on this page target the Whitechain Sepolia explorer at `https://explorer.testnet.whitechain.io` (chain id 1874). The API is public, read-only, and needs no API key. This dashboard reads chain-wide metrics, so it takes no address input.
</Note>

Every request below has a **Run this request** button. It sends that exact request from your browser to Whitechain Sepolia and prints the live response, so you can compare it with the sample response above it. The requests are read-only GET calls with no key and no wallet connection.

## What you can build

A network tracker reads two Blockscout API surfaces to show:

* Slow, average, and fast gas tiers in Gwei.
* Coin price in USD and BTC, and the reported coin supply.
* Transaction fees for the most recent completed day.
* Chain counters: total transactions, addresses, average block time, and a live chain head.

## Run the template

The [`whitechain-indexer-gas-tracker`](https://github.com/whitechain-labs/templates/tree/main/whitechain-indexer-gas-tracker) template implements every request on this page as a client-only React app. It reads the price, daily fees, and chain head from the RPC API, and the gas tiers and counters from REST API v2, on the two refresh timers described below. For the supply card it reads `total_supply` from `GET /api/v2/addresses` rather than the RPC action below, for the reason given in that section. Read it alongside this page to see the same calls in working code.

```bash Terminal theme={null}
npx degit whitechain-labs/templates/whitechain-indexer-gas-tracker indexer-gas-tracker
cd indexer-gas-tracker
pnpm install
pnpm dev
```

The app needs Node 20.19 or later. It connects no wallet and needs no API key, because every request it sends is one of the read-only calls documented below.

## API surfaces and base URLs

| Surface | Base URL | Use it for |
| - | - | - |
| RPC API (Etherscan-compatible) | `https://explorer.testnet.whitechain.io/api?module=…&action=…` | Coin price, supply, daily fees, and the chain head. |
| REST API v2 | `https://explorer.testnet.whitechain.io/api/v2` | The gas tiers and the aggregate network counters. |

The Etherscan-compatible module set is `account`, `block`, `contract`, `logs`, `stats`, `token`, and `transaction`. It has no gas-oracle action, so the gas tiers come from REST v2 `/stats`, which is the canonical source for them. A client written against the Etherscan API works against the RPC surface unchanged.

## Endpoint map

| Task | Interface | Request |
| - | - | - |
| Coin price (USD, BTC) | RPC API | `GET /api?module=stats&action=coinprice` |
| Reported coin supply | RPC API | `GET /api?module=stats&action=coinsupply` |
| Daily transaction fees | RPC API | `GET /api?module=stats&action=totalfees&date=YYYY-MM-DD` |
| Chain head | RPC API | `GET /api?module=block&action=eth_block_number` |
| Slow, average, fast gas tiers | REST API | `GET /api/v2/stats` (`gas_prices`) |
| Transactions, addresses, block time | REST API | `GET /api/v2/stats` |

## Reading headline metrics with the RPC API

Send GET requests to `/api` and select the call with `module` and `action`. Most actions return a `{ status, message, result }` envelope; `eth_block_number` returns a JSON-RPC envelope. Read `result` in both cases.

### Coin price

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api?module=stats&action=coinprice" \
  -H "accept: application/json"
```

```json Response theme={null}
{
  "status": "1",
  "message": "OK",
  "result": {
    "coin_usd": "57.38",
    "coin_btc": "0.00087178",
    "coin_usd_timestamp": "1784713251",
    "coin_btc_timestamp": "1784713251"
  }
}
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api?module=stats&action=coinprice" path="result.coin_usd" pathLabel="WBT price" unit="USD" runLabel="Read the price" />

Read `coin_usd` and `coin_btc` for the price cards. The timestamps are Unix seconds marking when each price was set.

### Reported coin supply

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api?module=stats&action=coinsupply" \
  -H "accept: application/json"
```

```json Response theme={null}
{ "status": "1", "message": "OK", "result": "116244402085899461811944309200000000000000000000000000000000" }
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api?module=stats&action=coinsupply" path="result" pathLabel="Reported coin supply, in whole WBT" runLabel="Read the supply" />

`result` is the coin supply in whole WBT as a string, not in wei. It is the sum of every address balance already divided by 10^18, so do not divide it again. Keep it a string and use big-integer or decimal math, because the value overflows a JavaScript number.

<Warning>
  On Whitechain Sepolia this figure is not a meaningful supply. Two addresses hold sentinel balances set at genesis, `2^256 - 1` wei and `2^248 - 1` wei, and they dominate the sum, which is why the value runs to 60 digits. Cross-check it against the balances in `GET /api/v2/addresses`, whose response also carries a `total_supply` field suitable for display. Present this endpoint as the reported coin supply rather than as a circulating supply.
</Warning>

### Daily transaction fees

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api?module=stats&action=totalfees&date=2026-07-21" \
  -H "accept: application/json"
```

```json Response theme={null}
{ "status": "1", "message": "OK", "result": "129648734562000000" }
```

`result` is the total fees paid that day, in wei. Here `129648734562000000` is about 0.1296 WBT.

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api?module=stats&action=totalfees&date={input}" inputLabel="Date (YYYY-MM-DD, a completed day)" inputValue="2026-07-21" path="result" pathLabel="Fees paid that day" format="wei" unit="WBT" />

<Note>
  `totalfees` is aggregated per completed day, so the current day reads `0` until it closes. Query the previous completed day for a real value.
</Note>

### Chain head

Poll this on a short interval for a live block-height indicator.

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api?module=block&action=eth_block_number" \
  -H "accept: application/json"
```

```json Response theme={null}
{ "jsonrpc": "2.0", "id": 1, "result": "0x2f915e" }
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api?module=block&action=eth_block_number" path="result" pathLabel="Chain head" format="hex" runLabel="Read the chain head" />

`result` is a hex quantity. `0x2f915e` is block `3117406`. Note this call returns a JSON-RPC envelope, not the `{ status, message, result }` shape the other RPC actions use.

## Reading gas tiers and counters with REST v2

One request to `/api/v2/stats` covers the gas tiers and the network counters.

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api/v2/stats" \
  -H "accept: application/json"
```

```json Response (trimmed) theme={null}
{
  "gas_prices": { "slow": 5, "average": 5, "fast": 5 },
  "gas_prices_update_in": 15173,
  "average_block_time": 1000,
  "total_blocks": "3110938",
  "total_transactions": "3594744",
  "total_addresses": "69677",
  "transactions_today": "86581",
  "network_utilization_percentage": 0.1178324,
  "gas_used_today": "4098201458",
  "coin_price": "57.38"
}
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api/v2/stats" path="gas_prices.average" pathLabel="Average gas price" unit="Gwei" runLabel="Read the gas tiers" />

| Field | Meaning | How to use it |
| - | - | - |
| `gas_prices.slow` / `.average` / `.fast` | Suggested gas prices in Gwei | Populate the three tier cards. Tiers are equal on a quiet chain, as here, where all three sit at the 5 gwei base fee floor. |
| `gas_prices_update_in` | Milliseconds until the next tier refresh | Time your own refresh to match. |
| `average_block_time` | Mean block time in milliseconds | Here 1000, so one block per second. |
| `total_blocks` / `total_transactions` / `total_addresses` | Chain totals as strings | Parse before formatting. |
| `network_utilization_percentage` | Recent utilization, 0 to 1 | Multiply by 100 for a percentage. |

## Errors

An RPC action that fails returns `status` `"0"` with a `message` describing the problem, while `result` is empty; check `status` before trusting `result`. An empty result set (for example, a day with no fees) is not an error. REST v2 returns HTTP `422` with an `errors` array for a malformed request, where each entry carries a `title`, a `source.pointer` naming the bad field, and a `detail` string.

## Refresh cadence

The metrics change at different rates, so refresh them on separate timers. Poll `eth_block_number` about once per second for a live head. Refetch the price, supply, fees, and `/api/v2/stats` together on a slower timer, for example every 15 seconds, which also lines up with `gas_prices_update_in`.

## Related

* [`whitechain-indexer-gas-tracker` template on GitHub](https://github.com/whitechain-labs/templates/tree/main/whitechain-indexer-gas-tracker)
* [Block explorer](/build/block-explorer/overview)
* [Wallet indexing example](/build/block-explorer/indexer-wallet)
* [Token dashboard indexing example](/build/block-explorer/indexer-token-dashboard)
* [Network fees](/learn/network/network-fees)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.