> ## Documentation Index
> Fetch the complete documentation index at: https://l2docs.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 circulating supply.
* Transaction fees for the most recent completed day.
* Chain counters: total transactions, addresses, average block time, and a live chain head.

## 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`                 |
| Circulating 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.

### Circulating 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="Circulating supply, raw" runLabel="Read the supply" />

`result` is the circulating supply in wei as a string. Divide by 10^18 for WBT, and use big-integer or decimal math because the value overflows a JavaScript number.

### 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": 4.38, "average": 4.38, "fast": 4.38 },
  "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 can be equal on a quiet chain, as here (all 4.38). |
| `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

* [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)
