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

# Base token dashboard with Chainstack RPC

> Build a small Base token dashboard: USD price, hourly candles and market activity from Serialized Data, plus a direct ERC-20 balance read at an explicit block through Chainstack RPC. Node.js, no dependencies.

> [!NOTE]
>
> This guide was written by the [Chainstack](https://chainstack.com/) team.

A token page needs two different kinds of information: market context and contract state. A chart can tell you where a token trades, but it does not tell you how many units a particular wallet holds at a specific block.

This guide builds a small Base token dashboard with Serialized Data for USD prices, hourly candles, and market activity, and Chainstack RPC for a direct ERC-20 balance read. You can enter a token contract, optionally add a wallet address, and inspect both without connecting a wallet or signing a transaction.

The result is a useful starting point for a wallet asset page or a support tool investigating a reported balance. If your application only needs prices and charts, Serialized alone may cover that requirement. The RPC layer becomes useful when you also need to query contract state at a stated block or add contract-specific behavior.

## What the dashboard does

* Shows the selected token's name, symbol, USD price, 24-hour volume, price change, and liquidity from Serialized.
* Plots the latest 24 available hourly USD close prices, with the underlying times and swap counts available as text.
* Reads an optional wallet's token balance and native ETH balance through Chainstack, using one selected Base block height.
* Displays market timestamps, candle pool identifiers, and the RPC block number and hash separately.
* Keeps working when one provider or panel fails, instead of presenting an error as a zero balance or price.

This is a local, manually refreshed, read-only example. It does not calculate PnL, quote swaps, execute trades, or claim that a token is safe. It supports standard ERC-20 contracts that implement `decimals()`; it is not a portfolio-discovery service.

## Why use these two APIs together?

[Serialized Data](https://docs.serialized.xyz/introduction.md) supplies indexed market information. The same market API calls work on Solana and 18 EVM chains by changing the `chain` parameter and supplying the appropriate token address. Reconstructing a chart directly from RPC would require you to identify the relevant pools, decode swaps, handle token orientation and decimals, and maintain a price history. The market API supplies that prepared dataset.

[Chainstack's Base RPC infrastructure](https://chainstack.com/build-better-with-base/) gives the application a separate path to contract state. In this example, the balance panel calls `balanceOf(wallet)` on the token contract, rather than inferring holdings from trade history. That is useful when transfers, airdrops, or other activity make a trading position different from a current contract balance.

The request path is simple: browser → local Node.js server → Serialized and Chainstack. Only the server holds credentials. The wallet address goes to the configured RPC endpoint; this example does not send it to Serialized because its market requests only need the token address.

## 1. Prepare your credentials and runtime

Use Node.js 22 or later. The example uses built-in `fetch`, the HTTP server, and the `--env-file` flag, so there are no npm dependencies to install.

Create a Serialized API key using the [authentication instructions](https://docs.serialized.xyz/authentication.md). Your first 100,000 credits are free when you add a card. Send the key directly in the `Authorization` header. Do not add a `Bearer` prefix. Serialized's public demo can be used from the documentation playground, but programmatic demo calls are restricted; the application below uses the production API and your own key.

Create a [Chainstack account](https://console.chainstack.com/) and obtain a Base mainnet HTTPS JSON-RPC endpoint. Follow the [node management instructions](https://docs.chainstack.com/docs/manage-your-node) and [Base tooling guide](https://docs.chainstack.com/docs/base-tooling) to locate the endpoint. Use the key-protected HTTPS URL for this example. A node password or a blockchain wallet private key is not required by this code.

Check that the endpoint is for Base mainnet. Its chain ID is 8453, represented as `0x2105` by `eth_chainId`. Base Sepolia is a different network. The server checks the returned chain ID before it reads a wallet.

Create a directory containing `server.mjs` and `index.html` from the complete source below. Create a `.env` file next to them:

```bash .env theme={null}
SERIALIZED_API_KEY=replace_with_your_key
CHAINSTACK_RPC_URL=https://your-base-mainnet-endpoint
PORT=3000
```

Keep `.env` out of version control. The RPC URL itself can contain a credential. Neither credential belongs in browser JavaScript, screenshots, or a public repository.

## 2. Understand the two market requests

The server sends these requests to `https://api.serialized.xyz`, with the raw API key in the `Authorization` header:

```http theme={null}
GET /v1/token?chain=evm:8453&address=TOKEN_ADDRESS
GET /v1/token/ohlcv?chain=evm:8453&address=TOKEN_ADDRESS&interval=1h&limit=24&quote=usd&fill=true
```

The [token details endpoint](https://docs.serialized.xyz/endpoints/token-details.md) returns a `{ data, meta }` envelope. The dashboard uses `data.priceUsd`, `data.stats24h.volumeUsd`, `data.stats24h.priceChangePct`, `data.liquidityUsd`, and `data.pool.address`. It validates the returned chain and token address before displaying the snapshot. A ticker is a display label, not a unique token identifier.

The [token candles endpoint](https://docs.serialized.xyz/endpoints/token-candles.md) returns `data` as an array. Its default quote is native, so `quote=usd` is essential for a USD chart. The server also rejects a candle response whose `meta.quote` is not `usd`. The chart uses each candle's `close`, rather than pretending to be a full candlestick chart.

Candle `time` is Unix seconds. `meta.asOf` and the token's `lastTradeAt` are milliseconds. Convert candle time with `new Date(candle.time * 1000)`; pass `meta.asOf` directly to `new Date()`. Mixing these units produces incorrect dates without necessarily crashing the application.

The default candle series follows Serialized's reference market, rather than every pool combined. The chart displays `meta.pools` and the snapshot displays its main market. A reference market can change between the two requests, so compare those identifiers before treating the panels as one market view. Historical series may also include a token's earlier launch-curve market.

`fill=true` can insert flat, zero-volume buckets when no trade occurred. The newest bucket can still be forming, and a young token may have fewer than 24 buckets. The heading deliberately says hourly close prices, not a guaranteed complete 24-hour trading history. Do not add these candle volumes and label the sum as token-wide 24-hour volume; that figure comes from `stats24h.volumeUsd`.

## 3. Read balances at an explicit block height

The wallet panel first calls `eth_chainId` and `eth_getBlockByNumber("latest", false)`. It then uses that returned hexadecimal block number for `eth_getCode`, both `eth_call` requests, and `eth_getBalance`. Reusing the number avoids silently reading each field from a different advancing latest block.

The contract calls use the ERC-20 ABI selectors below:

```text theme={null}
balanceOf(address): 0x70a08231 + wallet address left-padded to 32 bytes
decimals():         0x313ce567
```

The server decodes each 32-byte return value as `BigInt`. It formats the token amount using the contract's decimals, and formats the native ETH balance with 18 decimals. It never converts the raw uint256 balance to a JavaScript `Number`. For example, `1000000000000000001` base units at 18 decimals must display as `1.000000000000000001`, not `1`.

[ERC-20 defines `balanceOf` and marks `decimals` as optional](https://eips.ethereum.org/EIPS/eip-20). If `decimals` is absent, reverts, or returns an invalid word, the balance panel reports an error. It does not assume 18 or substitute Serialized's metadata. An ordinary zero balance still displays as `0`.

After the reads, the server fetches the selected block again and checks its hash. This detects a block replacement visible during that interval. It is a lightweight consistency check, not an atomic snapshot or a finality guarantee. The panel explicitly labels the selected latest block as not finalized. A stricter accounting workflow should use an appropriate safe or finalized block policy and account for reorgs.

See Base's [eth\_call reference](https://docs.base.org/base-chain/api-reference/ethereum-json-rpc-api/eth_call) and [eth\_getBalance reference](https://docs.base.org/base-chain/api-reference/ethereum-json-rpc-api/eth_getBalance) for the request and return formats. These are read-only RPC requests; they do not broadcast transactions or charge gas to the wallet.

## 4. Start the dashboard

After copying the complete source below and adding `.env`, run this command from the same directory:

```bash theme={null}
node --env-file=.env server.mjs
```

Open `http://127.0.0.1:3000`. The default token is DEGEN on Base, using the contract address from Serialized's own documentation. It is a sample identifier, not a token recommendation.

Click **Load dashboard** to fetch the market panels. To test the RPC panel, add a public wallet address you want to inspect and load again. No wallet connection or wallet ownership is needed for this read. A wallet that holds none of the selected token should return `0` if the call succeeds.

Expect a market snapshot, a USD close-price chart, and, when a wallet is supplied, token units, ETH, token decimals, block number, block hash, and the read time. Missing market fields display `Unavailable`. A provider error remains attached to its panel. The page clears the previous result before each load so that an old balance is not left beside a new token.

## 5. Interpret the result correctly

A market price and a wallet balance answer different questions. Serialized's `meta.asOf` describes its market response; the RPC panel identifies the block height used for the contract reads. Neither implies that both providers sampled the same instant.

The example does not multiply the current balance by the market price. That calculation would be an indicative mark, not a sell quote, cost basis, or PnL, and it would combine observations from different times. A real liquidation estimate needs executable quotes and position-size-aware liquidity assumptions.

Token liquidity is market context, not an amount every wallet can withdraw at the displayed price. Likewise, a recent response timestamp does not prove that a thinly traded token has traded recently. The dashboard displays `lastTradeAt` separately when available.

The balance is what this contract reports for this address and block. It does not prove that the token is transferable, redeemable, or safe. Token names and symbols are untrusted text. The UI uses `textContent` and creates chart elements directly, avoiding HTML interpolation from API metadata.

## Troubleshoot the common failures

* **Serialized HTTP 401 or 403**: check that the key is valid and sent as the raw `Authorization` value. If you tried the demo host from code, switch to the production host with a key.
* **Serialized HTTP 402**: inspect the account's billing or quota status in the portal. Do not retry an unresolved billing state in a tight loop.
* **HTTP 429**: wait before retrying. For a deployed version, respect `Retry-After` and implement bounded backoff. This local example performs no automatic retries.
* **Market 404 or missing chart**: verify the token contract and Base network, and test the same token in the relevant documentation playground. A deployed contract is not a guarantee of indexed markets or candle history.
* **Wrong RPC network**: replace the endpoint with a Base mainnet endpoint. The server deliberately refuses another chain rather than displaying unrelated balances.
* **No token contract or an `eth_call` failure**: check that the token input is a contract address, not a wallet or pool address, and that it implements the expected ERC-20 methods.
* **Block changed**: refresh the snapshot. For workflows that need stronger settlement assurances, change the block-selection policy rather than hiding this error.
* **One panel fails while others load**: inspect that panel's error. The server uses `Promise.allSettled` so a market outage does not become a fabricated RPC balance, or vice versa.

## Before turning the example into a public application

The server binds to `127.0.0.1` and exposes only the fixed dashboard workflow. It does not serve `.env` or act as a general RPC proxy. For an Internet-facing deployment, add application authentication, rate limits, concurrency limits, usage budgets, and appropriate HTTPS hosting before accepting arbitrary requests.

The current implementation sends two Serialized requests per load, plus seven RPC calls when a wallet is supplied. Serialized currently documents 2 credits for token details and 5 for candles, or 7 credits per successful two-panel market load. That is 7,000 credits for 1,000 loads before any caching. Verify the current [credit schedule](https://docs.serialized.xyz/authentication.md#credits) and [Chainstack request-unit rules](https://docs.chainstack.com/docs/request-units) before estimating spend; a Serialized credit is not a Chainstack request unit.

Keep manual refresh while prototyping. Before adding polling, cache shared market data by chain, token, interval, and quote, collapse simultaneous identical requests, and cache wallet snapshots separately with their block references. Market data can be shared across users; wallet-specific results need their own keys and privacy policy. Stop hidden-tab refreshes and avoid polling historical candles at the same cadence as a live price.

For streaming, follow Serialized's documented subscription and reconnection model rather than appending an unverified WebSocket snippet. Serialized Audit can be a later panel with its own credentials, billing, and risk interpretation. Neither feature is required to make this first guide useful.

## Complete source: server.mjs

Copy the following file in full. It contains the fixed API calls, input checks, balance decoding, per-panel error handling, and localhost server.

```javascript server.mjs theme={null}
import http from 'node:http';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';


export const TOKEN = '0x4ed4e862860bed51a9570b96d89af5e1b0efefed';
export const address = value => /^0x[0-9a-fA-F]{40}$/.test(value);
const word = value => {
  if (!/^0x[0-9a-fA-F]{64}$/.test(value)) throw Error('Invalid ABI uint256');
  return BigInt(value);
};
export function units(value, decimals) {
  const digits = value.toString().padStart(decimals + 1, '0');
  if (!decimals) return digits;
  const fraction = digits.slice(-decimals).replace(/0+$/, '');
  return digits.slice(0, -decimals) + (fraction ? '.' + fraction : '');
}


export function createServices({ key, rpcUrl, fetcher = fetch }) {
  let id = 0;
  async function json(url, options, label) {
    let response;
    try {
      response = await fetcher(url, {
        ...options, signal: AbortSignal.timeout(10000)
      });
    } catch { throw Error(label + ': connection failed or timed out'); }
    if (!response.ok) {
      const retry = response.status === 429 ? ' (retry later)' : '';
      throw Error(label + ': HTTP ' + response.status + retry);
    }
    try { return await response.json(); }
    catch { throw Error(label + ': invalid JSON'); }
  }
  async function serialized(path, params) {
    if (!key) throw Error('Set SERIALIZED_API_KEY in .env');
    const url = new URL(path, 'https://api.serialized.xyz');
    url.search = new URLSearchParams({ chain: 'evm:8453', ...params });
    const body = await json(url, { headers: { Authorization: key } }, 'Serialized');
    if (body.error || !Object.hasOwn(body, 'data')) {
      throw Error('Serialized: API error or missing data');
    }
    return body;
  }
  async function rpc(method, params) {
    if (!rpcUrl) throw Error('Set CHAINSTACK_RPC_URL in .env');
    const requestId = ++id;
    const body = await json(rpcUrl, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ jsonrpc: '2.0', id: requestId, method, params })
    }, 'Chainstack');
    if (body.id !== requestId || body.error || !Object.hasOwn(body, 'result')) {
      throw Error('Chainstack: ' + method + ' returned a JSON-RPC error');
    }
    return body.result;
  }
  async function balances(token, wallet) {
    if (BigInt(await rpc('eth_chainId', [])) !== 8453n) {
      throw Error('Wrong RPC network: use Base mainnet (8453)');
    }
    const block = await rpc('eth_getBlockByNumber', ['latest', false]);
    if (!block?.number || !block?.hash) throw Error('Block unavailable');
    const tag = block.number;
    const code = await rpc('eth_getCode', [token, tag]);
    if (code === '0x' || !code) throw Error('No token contract at this address');
    const call = data => rpc('eth_call', [{ to: token, data }, tag]);
    const [raw, decimalsWord, native] = await Promise.all([
      call('0x70a08231' + wallet.slice(2).toLowerCase().padStart(64, '0')),
      call('0x313ce567'), rpc('eth_getBalance', [wallet, tag])
    ]);
    const decimals = Number(word(decimalsWord));
    if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255) {
      throw Error('Invalid token decimals');
    }
    const after = await rpc('eth_getBlockByNumber', [tag, false]);
    if (after?.hash !== block.hash) throw Error('Block changed; refresh the snapshot');
    return {
      block: BigInt(tag).toString(), blockHash: block.hash,
      tokenBalance: units(word(raw), decimals), decimals,
      nativeBalance: units(BigInt(native), 18),
      readAt: Date.now(), finality: 'latest, not finalized'
    };
  }
  async function dashboard(token, wallet) {
    const results = await Promise.allSettled([
      serialized('/v1/token', { address: token }),
      serialized('/v1/token/ohlcv', {
        address: token, interval: '1h', limit: '24', quote: 'usd', fill: 'true'
      }),
      wallet ? balances(token, wallet) : Promise.resolve(null)
    ]);
    const output = { token, wallet, errors: {} };
    for (const [index, name] of ['market', 'candles', 'balances'].entries()) {
      const result = results[index];
      if (result.status === 'rejected') {
        output[name] = null; output.errors[name] = result.reason.message;
      } else output[name] = result.value;
    }
    if (output.market && (output.market.data?.chain !== 'evm:8453' ||
        output.market.data?.address?.toLowerCase() !== token.toLowerCase())) {
      output.market = null; output.errors.market = 'Unexpected token identity';
    }
    if (output.candles && (output.candles.meta?.quote !== 'usd' ||
        !Array.isArray(output.candles.data))) {
      output.candles = null; output.errors.candles = 'Unexpected candle shape or quote';
    }
    return output;
  }
  return { dashboard, balances };
}


export function createApp(services) {
  return http.createServer(async (req, res) => {
    res.setHeader('Cache-Control', 'no-store');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    try {
      const url = new URL(req.url, 'http://localhost');
      if (req.method !== 'GET') { res.writeHead(405); return res.end(); }
      if (url.pathname === '/') {
        res.setHeader('Content-Type', 'text/html; charset=utf-8');
        return res.end(await readFile(new URL('./index.html', import.meta.url)));
      }
      if (url.pathname !== '/api/dashboard') { res.writeHead(404); return res.end(); }
      const token = url.searchParams.get('token') || TOKEN;
      const wallet = url.searchParams.get('wallet') || '';
      if (!address(token) || (wallet && !address(wallet))) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        return res.end(JSON.stringify({ error: 'Enter valid 0x addresses (40 hex digits)' }));
      }
      const data = await services.dashboard(token, wallet);
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify(data));
    } catch {
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Unable to build the dashboard' }));
    }
  });
}
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
  const app = createApp(createServices({
    key: process.env.SERIALIZED_API_KEY, rpcUrl: process.env.CHAINSTACK_RPC_URL
  }));
  app.listen(Number(process.env.PORT || 3000), '127.0.0.1', () => {
    console.log('Dashboard: http://127.0.0.1:' + (process.env.PORT || 3000));
  });
}
```

## Complete source: index.html

Copy this file next to `server.mjs`. The chart is an SVG close-price line with labeled USD range and UTC endpoints. Expand the chart values to inspect the exact source timestamps and displayed close prices.

```html index.html theme={null}
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Base token dashboard</title>
<style>
body{font:16px system-ui;max-width:960px;margin:40px auto;padding:0 20px;color:#19212c}
label{display:block;margin:12px 0}input{display:block;width:100%;box-sizing:border-box;padding:10px}
button{padding:10px 20px;cursor:pointer}section{border-top:1px solid #ddd;margin-top:24px;padding-top:12px}
pre{white-space:pre-wrap;overflow-wrap:anywhere}svg{width:100%;height:220px;background:#f3f6fa}
.error{color:#a32020}small{color:#555}
</style>
<h1>Base token dashboard</h1>
<p>Serialized market data and a direct wallet balance snapshot through Chainstack.</p>
<form id="form">
<label>Token contract on Base mainnet
<input id="token" value="0x4ed4e862860bed51a9570b96d89af5e1b0efefed" required></label>
<label>Wallet address (optional, read-only)<input id="wallet" placeholder="0x..."></label>
<button id="load">Load dashboard</button>
</form>
<p id="status" role="status" aria-live="polite"></p>
<section><h2>Market snapshot</h2><pre id="market">Load a token to begin.</pre></section>
<section><h2>Hourly close price (USD)</h2>
<p id="chart-note"></p>
<svg id="chart" viewBox="0 0 800 220" role="img" aria-label="Token hourly USD close prices"></svg>
<details><summary>Chart values and times (UTC)</summary><pre id="chart-data"></pre></details>
</section>
<section><h2>Wallet balance at a Base block</h2><pre id="balances">Enter a wallet to read balances.</pre></section>
<script type="module">
const el = id => document.getElementById(id);
const number = value => typeof value === 'number' && Number.isFinite(value);
const fmt = value => number(value) ? value.toLocaleString('en-US', { maximumSignificantDigits: 8 }) : 'Unavailable';
const time = ms => number(ms) && Number.isFinite(new Date(ms).getTime()) ? new Date(ms).toISOString() : 'Unavailable';
el('form').addEventListener('submit', async event => {
  event.preventDefault(); el('load').disabled = true; el('status').textContent = 'Loading…';
  for (const id of ['market','chart-note','chart-data','balances']) el(id).textContent = '';
  el('chart').replaceChildren();
  try {
    const query = new URLSearchParams({token:el('token').value.trim(),wallet:el('wallet').value.trim()});
    const response = await fetch('/api/dashboard?' + query);
    const result = await response.json();
    if (!response.ok) throw Error(result.error || 'Request failed');
    const m = result.market?.data;
    el('market').textContent = m ? [
      (m.name || 'Unnamed token') + ' (' + (m.symbol || 'unknown') + ')',
      'Contract: ' + result.token, 'Price USD: ' + fmt(m.priceUsd),
      '24h volume USD: ' + fmt(m.stats24h?.volumeUsd),
      '24h price change %: ' + fmt(m.stats24h?.priceChangePct),
      'Token liquidity USD: ' + fmt(m.liquidityUsd),
      'Main market: ' + (m.pool?.address || 'Unavailable'),
      'Snapshot as of: ' + time(result.market.meta?.asOf),
      'Last trade: ' + time(m.lastTradeAt)
    ].join('\n') : (result.errors.market || 'Market unavailable');
    const b = result.balances;
    el('balances').textContent = b ? [
      'Token units: ' + b.tokenBalance, 'ETH: ' + b.nativeBalance,
      'Token decimals: ' + b.decimals, 'Block: ' + b.block,
      'Block hash: ' + b.blockHash, 'Read at: ' + time(b.readAt), b.finality
    ].join('\n') : (result.errors.balances || 'Enter a wallet to read balances.');
    const rows = (result.candles?.data || []).filter(c =>
      number(c.time) && number(c.close) && c.close >= 0).sort((a,b) => a.time-b.time);
    const points = [...new Map(rows.map(c=>[c.time,c])).values()];
    const meta = result.candles?.meta;
    el('chart-note').textContent = result.errors.candles || (points.length ?
      points.length + ' hourly buckets; as of ' + time(meta?.asOf) +
      '. Pools: ' + (meta?.pools || []).join(', ') +
      '. Filled buckets may have no trades; the newest bucket can change.' : 'No candle data.');
    el('chart-data').textContent = points.map(c=>time(c.time*1000) +
      ' | close USD ' + fmt(c.close) + ' | swaps ' + (c.trades ?? 'Unavailable')).join('\n');
    if (points.length) {
      const first=points[0].time, last=points.at(-1).time;
      const low=Math.min(...points.map(c=>c.close)), high=Math.max(...points.map(c=>c.close));
      const xy=points.map(c=>[20+(c.time-first)/(last-first||1)*760,
        high===low ? 110 : 200-(c.close-low)/(high-low)*180]);
      const line=document.createElementNS('http://www.w3.org/2000/svg','polyline');
      line.setAttribute('points',xy.map(p=>p.join(',')).join(' '));
      line.setAttribute('fill','none'); line.setAttribute('stroke','#2860bd');
      line.setAttribute('stroke-width','2'); el('chart').append(line);
      if(points.length===1){
        const dot=document.createElementNS('http://www.w3.org/2000/svg','circle');
        dot.setAttribute('cx',xy[0][0]);dot.setAttribute('cy',xy[0][1]);dot.setAttribute('r','4');el('chart').append(dot);
      }
      const caption=(x,y,text)=>{
        const t=document.createElementNS('http://www.w3.org/2000/svg','text');
        t.setAttribute('x',x);t.setAttribute('y',y);t.setAttribute('font-size','12');
        t.textContent=text;el('chart').append(t);
      };
      caption(20,15,'USD '+fmt(low)+' to '+fmt(high));
      caption(20,216,time(first*1000).slice(0,16)+' UTC');
      caption(570,216,time(last*1000).slice(0,16)+' UTC');
    }
    el('status').textContent = Object.keys(result.errors).length ? 'Loaded with partial errors. See each panel.' : 'Loaded. Refresh manually when needed.';
  } catch(error) {el('status').textContent=error.message;}
  finally {el('load').disabled=false;}
});
</script>
</html>
```

## Further reading

* [Serialized token details](https://docs.serialized.xyz/endpoints/token-details.md)
* [Serialized token candles](https://docs.serialized.xyz/endpoints/token-candles.md)
* [Serialized authentication and limits](https://docs.serialized.xyz/authentication.md)
* [Chainstack Base tooling](https://docs.chainstack.com/docs/base-tooling)
* [Base network chain ID](https://docs.base.org/base-chain/api-reference/ethereum-json-rpc-api/eth_chainId)
* [ERC-20 specification](https://eips.ethereum.org/EIPS/eip-20)


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