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

# Wallet indexing example

> Read balances, token holdings, and transaction history for any address on Whitechain Sepolia with the Blockscout REST v2 API and ETH JSON-RPC, 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, so a wallet view runs fully client-side with no backend and no node of your own. Replace the example address `0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09` with the address you want to read.
</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. Each button carries its own address field, so editing the address in one runner does not change the others. The requests are read-only GET and POST calls with no key and no wallet connection, and nothing you enter leaves your browser except the address in the URL.

## What you can read

A wallet view combines two Blockscout API surfaces to answer four questions about an address:

* What is its balance? Both the indexed balance and the live node balance, in WBT.
* What tokens does it hold? Every ERC-20, ERC-721, and ERC-1155 balance, with symbol and decimals.
* What has it done? A paginated transaction history with method, direction, value, fee, and status.
* Where is the chain now? The latest block height, for a live head indicator.

## API surfaces and base URLs

| Surface      | Base URL                                             | Use it for                                                                            |
| ------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| REST API v2  | `https://explorer.testnet.whitechain.io/api/v2`      | The indexed view. Returns decoded JSON: address record, token balances, transactions. |
| ETH JSON-RPC | `https://explorer.testnet.whitechain.io/api/eth-rpc` | The live node view. Standard Ethereum methods that return canonical chain state.      |

REST v2 answers "what does the indexer know", and it returns rich pre-decoded objects. JSON-RPC answers "what is true on the node right now", and it returns raw hex quantities. A wallet view reads both and shows them side by side.

## Endpoint map

| Task                     | Interface   | Request                                            |
| ------------------------ | ----------- | -------------------------------------------------- |
| Address summary and type | REST API    | `GET /api/v2/addresses/{hash}`                     |
| Token holdings           | REST API    | `GET /api/v2/addresses/{hash}/token-balances`      |
| Transaction history      | REST API    | `GET /api/v2/addresses/{hash}/transactions`        |
| Live native balance      | ETH RPC API | `POST /api/eth-rpc` with `eth_getBalance`          |
| Nonce (outbound count)   | ETH RPC API | `POST /api/eth-rpc` with `eth_getTransactionCount` |
| Chain head               | ETH RPC API | `POST /api/eth-rpc` with `eth_blockNumber`         |

## Reading with the REST API v2

Send GET requests with an `accept: application/json` header. The examples below use the real testnet address above; the responses are trimmed to the fields a wallet view reads.

### Address summary

Use this to show the indexed balance and to label the address a wallet or a contract.

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

```json Response (trimmed) theme={null}
{
  "hash": "0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09",
  "coin_balance": "177504708847852442",
  "exchange_rate": "57.52",
  "is_contract": false,
  "is_verified": false,
  "ens_domain_name": null,
  "block_number_balance_updated_at": 3113184
}
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api/v2/addresses/{input}" inputLabel="Address" inputValue="0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" path="coin_balance" pathLabel="Indexed balance" format="wei" unit="WBT" />

| Field             | Meaning                                 | How to use it                                                         |
| ----------------- | --------------------------------------- | --------------------------------------------------------------------- |
| `coin_balance`    | Indexed native balance, in wei          | Divide by 10^18 for WBT. Here: 0.1775 WBT.                            |
| `exchange_rate`   | WBT price in USD at index time          | Multiply by the WBT amount for a USD estimate. Here: about 10.21 USD. |
| `is_contract`     | Whether the address holds contract code | Label the address a contract or a wallet.                             |
| `is_verified`     | Whether a contract's source is verified | Show a verified badge for contracts.                                  |
| `ens_domain_name` | Primary name, or `null`                 | Show in place of the raw hash when present.                           |

### Token holdings

Use this to list every fungible and non-fungible balance the address holds.

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

```json Response (trimmed) theme={null}
[
  {
    "value": "10000000000000000000",
    "token_id": null,
    "token": {
      "address_hash": "0x071c373d58A5290982a0E916D529a27849baE6e0",
      "name": "USDW",
      "symbol": "USDW",
      "decimals": "6",
      "type": "ERC-20"
    }
  }
]
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api/v2/addresses/{input}/token-balances" inputLabel="Address" inputValue="0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" path="length" pathLabel="Token balances returned" />

The response is a flat array, one entry per token. Format each `value` against its own `token.decimals` (they differ per token, so never assume 18). For `ERC-721` and `ERC-1155` entries, `token_id` is set and `type` names the standard. An empty array (`[]`) means the address holds no tokens.

### Transaction history

Use this for the activity list. The response is newest first.

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

```json Response (trimmed to one item) theme={null}
{
  "items": [
    {
      "hash": "0xf3d44e187cf95cf227a253cf983c45b8ec76eba1c3177a79b46e31de9083c487",
      "timestamp": "2026-07-16T11:13:21.000000Z",
      "from": { "hash": "0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" },
      "to": { "hash": "0xC880eF22c01184a3Db08F2c306684311C48cB495", "name": "Storage", "is_contract": true },
      "value": "0",
      "fee": { "type": "actual", "value": "134555036000360" },
      "method": "store",
      "result": "success",
      "block_number": 2604033,
      "confirmations": 512671,
      "transaction_types": ["contract_call"]
    }
  ],
  "next_page_params": null
}
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api/v2/addresses/{input}/transactions" inputLabel="Address" inputValue="0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" path="items.0.method" pathLabel="Method of the newest transaction" />

| Field                            | Meaning                                                | How to use it                                                                                                 |
| -------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `from.hash` / `to.hash`          | Sender and recipient                                   | Compare with the queried address to set direction: outbound if it matches `from`, inbound if it matches `to`. |
| `to.name`                        | Contract name when the target is a known contract      | Show "store on Storage" instead of a bare hash.                                                               |
| `value`                          | Native WBT moved, in wei                               | Divide by 10^18. `0` for pure contract calls.                                                                 |
| `fee.value`                      | Fee paid, in wei (`fee.type` is `actual` or `maximum`) | Divide by 10^18 for the WBT fee.                                                                              |
| `method`                         | Decoded function name, or `null`                       | Show the action; `null` for a plain transfer or contract creation.                                            |
| `result`                         | `success` or an error string                           | Drive a status badge.                                                                                         |
| `block_number` / `confirmations` | Inclusion height and depth                             | Link to the block; show confirmation count.                                                                   |

You can narrow the history with query parameters:

| Parameter | Endpoint                                | Values                                                    | Effect                                                           |
| --------- | --------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
| `filter`  | `/api/v2/addresses/{hash}/transactions` | `to`, `from`                                              | Keep only inbound or only outbound transactions for the address. |
| `filter`  | `/api/v2/transactions` (chain-wide)     | `pending`, `validated`                                    | List mempool or mined transactions across the chain.             |
| `type`    | `/api/v2/transactions`                  | `token_transfer`, `contract_creation`, `contract_call`    | Keep only that transaction category.                             |
| `method`  | `/api/v2/transactions`                  | e.g. `approve`, `transfer`, `multicall`, `mint`, `commit` | Keep only calls to that decoded method.                          |

For example, outbound transactions only:

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api/v2/addresses/0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09/transactions?filter=from" \
  -H "accept: application/json"
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api/v2/addresses/{input}/transactions?filter=from" inputLabel="Address" inputValue="0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" path="items.length" pathLabel="Outbound transactions on this page" />

## Reading with the ETH JSON-RPC API

Send POST requests to `https://explorer.testnet.whitechain.io/api/eth-rpc` with a JSON-RPC body. Every method here returns a hex quantity in `result`; convert it to a number before display.

### Live native balance

Compare this with the indexed `coin_balance` from the address summary. They match once the indexer catches up to the node.

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api/eth-rpc" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09","latest"]}'
```

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

`0x2769f979cd5db9a` is `177504708847852442` wei, or about 0.1775 WBT.

<ApiRunner method="POST" url="https://explorer.testnet.whitechain.io/api/eth-rpc" body="{&#x22;jsonrpc&#x22;:&#x22;2.0&#x22;,&#x22;id&#x22;:1,&#x22;method&#x22;:&#x22;eth_getBalance&#x22;,&#x22;params&#x22;:[&#x22;{input}&#x22;,&#x22;latest&#x22;]}" inputLabel="Address" inputValue="0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" path="result" pathLabel="Live balance" format="wei" unit="WBT" />

### Nonce

The nonce is the number of transactions the address has sent. Read it to show account activity or to build a raw transaction.

```bash Request theme={null}
curl "https://explorer.testnet.whitechain.io/api/eth-rpc" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount","params":["0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09","latest"]}'
```

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

`0x1e` is `30`, so this address has sent 30 transactions.

<ApiRunner method="POST" url="https://explorer.testnet.whitechain.io/api/eth-rpc" body="{&#x22;jsonrpc&#x22;:&#x22;2.0&#x22;,&#x22;id&#x22;:1,&#x22;method&#x22;:&#x22;eth_getTransactionCount&#x22;,&#x22;params&#x22;:[&#x22;{input}&#x22;,&#x22;latest&#x22;]}" inputLabel="Address" inputValue="0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09" path="result" pathLabel="Nonce" format="hex" />

### 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/eth-rpc" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'
```

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

`0x2f80e0` is block `3113184`.

<ApiRunner method="POST" url="https://explorer.testnet.whitechain.io/api/eth-rpc" body="{&#x22;jsonrpc&#x22;:&#x22;2.0&#x22;,&#x22;id&#x22;:1,&#x22;method&#x22;:&#x22;eth_blockNumber&#x22;,&#x22;params&#x22;:[]}" path="result" pathLabel="Chain head" format="hex" runLabel="Read the chain head" />

## Pagination

REST v2 list endpoints return `next_page_params`. When it is `null`, you have the last page. When it is an object, pass its fields back as query parameters on the same endpoint to get the next page.

```bash Request (next page of transactions) theme={null}
curl "https://explorer.testnet.whitechain.io/api/v2/addresses/0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09/transactions?block_number=2427207&index=1" \
  -H "accept: application/json"
```

<ApiRunner method="GET" url="https://explorer.testnet.whitechain.io/api/v2/addresses/0xA439Ad519046CCd7056Ddf74fbaAc99d740Bdf09/transactions?block_number={input}&index=1" inputLabel="block_number cursor" inputValue="2427207" path="items.0.block_number" pathLabel="Block of the first item on this page" />

The exact keys inside `next_page_params` vary by endpoint (transactions page by `block_number` and `index`), so copy whatever keys the previous response returned rather than hardcoding them.

## Errors

A malformed address or query parameter returns HTTP `422` with an `errors` array describing the bad input. Run any request above with a broken address to see it:

```json Response (HTTP 422) theme={null}
{
  "errors": [
    {
      "title": "Invalid value",
      "source": { "pointer": "/address_hash_param" },
      "detail": "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/"
    }
  ]
}
```

A well-formed address the indexer has never seen is not an error: it returns HTTP `200` with `null` fields. JSON-RPC calls return HTTP `200` with an `error` object instead of `result` when the request is invalid, so check for `error` before reading `result`. Handle every path so a bad address shows a clear message rather than an empty view.

## Load sequence

For a full wallet view, issue all six requests in parallel: the three REST reads (summary, token balances, transactions) and the three JSON-RPC reads (balance, nonce, chain head). None depend on another, so a single batch fills every card at once. Poll only `eth_blockNumber` afterward for the live head; refetch the rest when the user changes the address.

## Related

* [Block explorer](/build/block-explorer/overview)
* [Token dashboard indexing example](/build/block-explorer/indexer-token-dashboard)
* [Gas and network tracker example](/build/block-explorer/indexer-gas-tracker)
* [Blockscout API reference](https://explorer.testnet.whitechain.io/api-docs)
