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

# Build a Trading Terminal

> Every pane of an Axiom-style terminal mapped to the endpoint that powers it: discovery columns, trending, search, a token page with a live chart and tape, watchlist and portfolio. REST for state, one WebSocket for everything live.

This guide walks through a complete trading terminal, pane by pane: the discovery columns, the trending tab, the search bar, a token page with a live chart, a live tape and due-diligence panels, a watchlist and a portfolio view. Every pane maps to one or two endpoints, every payload shown here is the real shape, and everything works identically on all 18 EVM chains and Solana: build the page once, ship it everywhere.

| Terminal pane | Powered by |
| - | - |
| Discovery columns (new / bonding / graduated) | [`GET /v1/pulse`](/endpoints/pulse) |
| Trending tab | [`GET /v1/screener`](/endpoints/market-screener) |
| Search bar | [`GET /v1/search`](/endpoints/token-search) |
| Token header | [`GET /v1/token`](/endpoints/token-details) + [`GET /v1/token/stats`](/endpoints/token-stats-windowed) |
| Candlestick chart | [`GET /v1/token/ohlcv`](/endpoints/candles-ohlcv) |
| Trade tape | [`GET /v1/token/trades`](/endpoints/trades-tape) + the [`trades` stream](/streams/trades-stream) |
| Holders, top traders, security | [`/v1/token/holders`](/endpoints/token-holders) · [`/v1/token/top-traders`](/endpoints/token-top-traders) · [`/v1/token/security`](/endpoints/token-security) |
| Watchlist | [`POST /v1/token`](/endpoints/token-details-batch) + [`POST /v1/token/sparklines`](/endpoints/token-sparklines) |
| Portfolio | [`GET /v1/wallet/positions`](/endpoints/wallet-positions) + [`GET /v1/wallet/pnl`](/endpoints/wallet-pnl) |
| Everything live | [`wss://api.serialized.xyz/v1/stream`](/streams/overview) |

<Note>Every endpoint page in this doc has an interactive playground that runs live without a key: open one and hit Send to see the full response next to the code below.</Note>

## Ground rules

Three conventions carry the whole build:

* **Auth** is one header, `Authorization: YOUR_API_KEY`, on every call.
* **Every response** is `{ data, meta }`, camelCase, millisecond timestamps, and `null` always means unknown, never a fabricated zero.
* **Chains** are `evm:<id>` (`evm:8453` for Base, `evm:1` for Ethereum) or `solana`; a token is always `(chain, address)`.

One helper is all the plumbing you need:

```typescript lib/serialized.ts theme={null}
const API = 'https://api.serialized.xyz/v1';
const KEY = process.env.SERIALIZED_API_KEY!;

export async function sd<T>(
  path: string,
  params: Record<string, string | number | boolean | undefined> = {},
): Promise<{ data: T; meta: Record<string, any> }> {
  const qs = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) if (v !== undefined) qs.set(k, String(v));
  const res = await fetch(`${API}${path}?${qs}`, { headers: { Authorization: KEY } });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${res.status} ${error.code}: ${error.message}`);
  }
  return res.json();
}
```

Errors are machine-readable (`INVALID_PARAM`, `NOT_FOUND`, `RATE_LIMITED`, the [full catalog](/errors)), and you are only billed for `2xx` responses: your own errors never cost credits.

## The discovery columns

The three launchpad lifecycle columns every terminal opens on are one endpoint, one call per column: `view=new` (created under 24h), `view=bonding` (active curves, sorted by bonding progress), `view=graduated` (migrated in the last 7 days). Around 90 launchpads are tracked natively across chains, and EVM and Solana merge in a single call: list the chains you want in `chains` and the feed comes back ranked.

```typescript discovery.ts theme={null}
type View = 'new' | 'bonding' | 'graduated';

const column = (view: View) =>
  sd<PulseCard[]>('/pulse', { view, chains: 'evm:8453,solana' });

async function refreshDiscovery() {
  const [fresh, bonding, graduated] = await Promise.all([
    column('new'), column('bonding'), column('graduated'),
  ]);
  render({ fresh: fresh.data, bonding: bonding.data, graduated: graduated.data });
}

setInterval(refreshDiscovery, 3_000);
```

Each card already carries everything the column renders: identity and `metadata` (icon on our CDN, socials, `dexPaid`), live figures over the view window (`priceChangePct`, `volumeUsd`, `txns`, `buys`, `sells`), `feesUsd` since the pool's creation, `marketCapUsd`, `liquidityUsd`, `bondingProgress`, `migrated`, `ageSeconds`, and the `creator` wallet with its display name when known. No second call per row.

For sub-second discovery, make the columns live instead of polled: subscribe [`token-updates`](/streams/token-updates-stream) on the cards currently on screen (more on the stream below).

## The trending tab

Discovery beyond launchpads is [`GET /v1/screener`](/endpoints/market-screener): every tradable market, ranked and paginated. `sortBy=trending` (the default) weighs 24h activity by real participation, so wash-traded markets sink; `sortBy=volume` with `window=5m|1h|6h|24h` gives you the "top last hour" tab; `sortBy=createdAt` is the new-markets firehose.

```bash theme={null}
curl 'https://api.serialized.xyz/v1/screener?chains=evm:8453,solana&limit=50' \
  -H 'Authorization: YOUR_API_KEY'
```

Every row is chart-ready: price, market cap, liquidity, and a `stats` object with the `5m`/`1h`/`6h`/`24h` windows (volume, buys, sells, price change), so the whole trending table is one call.

## The search bar

One endpoint handles both things users paste into a terminal: an exact address (token or pool, EVM hex or Solana base58) and free text on name or symbol. Leave `chain` out and it searches EVM and Solana in parallel, merged by USD liquidity, so the search bar is chain-agnostic by default.

```typescript theme={null}
const { data } = await sd<SearchHit[]>('/search', { q: input });
// each hit: identity + decimals, priceUsd, liquidityUsd, marketCapUsd,
// volume24hUsd, priceChange1hPct/24hPct, and the token's main pool
```

Debounce it (say 300 ms) and route straight to the token page on an exact address hit.

## The token page

### The header

Two calls fill the whole top of the page. [`GET /v1/token`](/endpoints/token-details) is the snapshot: price (native and USD), market cap, FDV, liquidity, supplies, the token's main pool, launchpad state (`bondingProgress`, `graduatedAt`), deployer, socials and icon, plus `stats24h`. [`GET /v1/token/stats`](/endpoints/token-stats-windowed) adds the windowed row of every terminal header: `5m` / `1h` / `4h` / `6h` / `24h`, each with volume, buys/sells split and price change, in one call.

```typescript token-page.ts theme={null}
const [token, stats] = await Promise.all([
  sd<Token>('/token', { chain, address }),
  sd<TokenStats>('/token/stats', { chain, address, windows: '5m,1h,4h,6h,24h' }),
]);
```

### The chart

[`GET /v1/token/ohlcv`](/endpoints/candles-ohlcv) serves chart-ready candles from `1s` to `1M`, up to 2000 per page. Two behaviors matter for a terminal chart:

* **The series survives graduation.** By default candles follow the pool the token is priced from, with the bonding-curve history stitched in front: a token that migrated from Pump.fun or a Virtuals curve draws one continuous line, no gap at migration, nothing to handle on your side.
* **The axis is dense.** Every bucket is present; a quiet bucket is a flat zero-volume candle at the previous close, so thin tokens still draw a proper chart.

Candles drop straight into a TradingView datafeed: `time` is unix seconds, intervals use the same keys (aliases like `1min` and `60` are accepted), and paging backwards is `endTime` plus `meta.hasMore`:

```typescript datafeed.ts theme={null}
async function getBars(chain: string, address: string, interval: string,
                       { to, countBack, firstDataRequest }: PeriodParams) {
  const { data, meta } = await sd<Candle[]>('/token/ohlcv', {
    chain, address, interval,
    endTime: firstDataRequest ? undefined : to,   // unix seconds
    limit: Math.min(countBack || 500, 2000),
  });
  return {
    bars: data.map(c => ({ time: c.time * 1000, open: c.open, high: c.high,
                           low: c.low, close: c.close, volume: c.volumeToken })),
    meta: { noData: data.length === 0 && !meta.hasMore },
  };
}
```

Prices are native-quoted by default. On Solana pass `quote=usd` for USD candles; on EVM multiply by the native USD spot from [`GET /v1/prices/native`](/endpoints/native-prices), one cached call for all your charts.

For the complete TradingView wiring (resolveSymbol, the microcap `pricescale` trick, live bars, Lightweight Charts), see [TradingView Charts](/guides/tradingview-charts).

### The tape

[`GET /v1/token/trades`](/endpoints/trades-tape) seeds the tape: token-wide by default, newest first, cursor pagination for infinite scroll (`meta.nextCursor` until `hasMore` is `false`). Each row is ready to render (trimmed here):

```json theme={null}
{
  "id": "0x9d6dd98095ebcb254576b23b2269edf8fa984b581404209aa2a5fe3a14431bdf:289",
  "at": 1786718835000,
  "maker": "0x95d955179a7cd45aeef394ed39f6a8d8b1bd1e09",
  "isBuy": true,
  "amountToken": 77.05662024406023,
  "priceNative": 5.4747124708872e-7,
  "volumeUsd": 0.08021081608014609,
  "isProTrader": false,
  "isSniper": false,
  "poolAddress": "0x0ca6485b7e9cf814a3fd09d81672b07323535b64"
}
```

`maker` is the wallet actually trading, resolved through relays, solvers and smart accounts, with `isProTrader` / `isSniper` badges on every row and `isWash` on Solana: the tape is attribution-correct out of the box. Filters cover side, trade size, date range and maker lists when you add tape controls.

### The side panels

The due-diligence column is three endpoints, one call each, same shape on EVM and Solana:

* [`/v1/token/holders`](/endpoints/token-holders): top holders with share of supply, full PnL, behavioral labels and named entities (the "Coinbase" row), plus `meta.totalHolders` for the header count.
* [`/v1/token/top-traders`](/endpoints/token-top-traders): ranked PnL leaderboard, lifetime or any rolling window from `1h` to `30d`, with win rate and dev/sniper/pro badges.
* [`/v1/token/security`](/endpoints/token-security): the pre-trade checklist in one shape: mint/freeze authorities, top-10 concentration (exchange custody excluded), dev/sniper/bundler holdings, LP burn/lock, taxes, `dexPaid`.

Add [`/v1/token/pools`](/endpoints/token-pools) for the market selector: rank 1 is the exact pool the terminal is pricing from, the rest ranked by 24h volume, each with its own stats.

## Make it live

Everything above renders a page; one WebSocket makes it move. Open a single connection for the whole app, authenticate, then attach subscriptions per pane as the user navigates: events reuse the exact REST shapes, so the parsing code you just wrote handles the stream unchanged.

```typescript stream.ts theme={null}
const ws = new WebSocket('wss://api.serialized.xyz/v1/stream');
const sub = (channel: string, id: string, params: object) =>
  ws.send(JSON.stringify({ op: 'subscribe', channel, id, params }));

ws.onopen = () => ws.send(JSON.stringify({ op: 'auth', apiKey: KEY }));

ws.onmessage = (m) => {
  const frame = JSON.parse(m.data as string);
  if (frame.op === 'auth.ok') {
    setInterval(() => ws.send(JSON.stringify({ op: 'ping' })), 30_000);
    sub('trades', 'tape', { chain, address });          // every trade, as it lands
    sub('token-updates', 'header', { chain, address }); // price/mcap/stats24h, ≤1/s
  }
  if (frame.op === 'event' && frame.id === 'tape') prependTrade(frame.data);
  if (frame.op === 'event' && frame.id === 'header') patchHeader(frame.data);
};
```

How each pane goes live:

* **Header**: [`token-updates`](/streams/token-updates-stream) pushes the mutable fields of `/v1/token` (price, market cap, liquidity, bonding progress, `stats24h`), throttled to one event per second per token. Patch them over the snapshot.
* **Tape**: [`trades`](/streams/trades-stream) events are the exact tape row above. Prepend, and dedup by `id`.
* **Chart**: fold each trade into the forming candle (`bucket = floor(at / 1000 / intervalSec) * intervalSec`; new bucket opens at the previous close).
* **Discovery / watchlist rows**: one `token-updates` subscription per visible card; unsubscribe by id as rows scroll out, the connection stays.

Two production patterns:

* **Reconnect**: delivery is at-most-once, so on reconnect re-subscribe, then backfill the gap with `GET /v1/token/trades?fromAt=<last event at>` and dedup by `id`. The tape never misses or doubles a trade.
* **Cost control**: every subscription accepts `maxUpdatesPerMinute`, which caps delivered events (you always get the latest state) and therefore cost. Streams bill 1 credit per event actually delivered, the connection itself is free.

Per key you have 10 concurrent connections, 50 subscriptions per connection and 100 distinct tokens across channels, raised on request: one connection per browser tab is the comfortable pattern.

## The watchlist

Batch twins keep pinned lists at one round-trip per refresh, whatever the size: [`POST /v1/token`](/endpoints/token-details-batch) serves up to 25 full snapshots per call, EVM and Solana mixed freely, per-item error slots so one dead token never breaks the list. [`POST /v1/token/sparklines`](/endpoints/token-sparklines) draws the row charts: up to 100 tokens per call, best pool resolved for you, curve history stitched so freshly graduated tokens draw one continuous line.

```bash theme={null}
curl -X POST 'https://api.serialized.xyz/v1/token/sparklines' \
  -H 'Authorization: YOUR_API_KEY' -H 'Content-Type: application/json' \
  -d '{"chain":"evm:8453","tokens":["0x4ed4...efed","0xb200...b301"],"timeframe":"24h"}'
```

## The portfolio tab

[`GET /v1/wallet/positions`](/endpoints/wallet-positions) is the whole holdings screen in one call: every position with amount, live USD value, average entry, realized + unrealized + total PnL and `holdingSince`, plus `meta.summary` for the wallet-level totals at the top. Add `includeNative=true&includeStables=true` and the SOL/ETH and stablecoin rows are read on-chain at request time, so the total is the real account value.

```bash theme={null}
curl 'https://api.serialized.xyz/v1/wallet/positions?chain=solana&wallet=WALLET&includeNative=true&includeStables=true' \
  -H 'Authorization: YOUR_API_KEY'
```

[`GET /v1/wallet/pnl`](/endpoints/wallet-pnl) draws the performance curve (daily realized PnL with win rate, any period up to `all`), and [`/v1/wallet/closed-positions`](/endpoints/wallet-closed-positions) fills the trade-history tab with every completed round trip.

## What a session costs

Credits map one-to-one to the calls above, so a terminal's burn is easy to model before you ship it:

| User action | Calls | Credits |
| - | - | - |
| Discovery refresh (3 columns) | 3 × `pulse` | 3 |
| Trending page | 1 × `screener` | 1 |
| Search (debounced) | 1 × `search` | 5 |
| Token page open (header, chart, tape) | `token` + `stats` + `ohlcv` + `trades` | 8 |
| Full due diligence (+ holders, top traders, security) | 3 more calls | + 14 |
| Watchlist refresh, 20 tokens with sparklines | 2 batch calls | 40 |
| Portfolio open | `positions` + `pnl` | 3 |
| Live token page | stream events | 1 per delivered event |

Only `2xx` responses are billed, batches bill per item, and every billable response carries `X-Credits-Remaining` so your backend can watch its own burn. Plans and rate limits are on [Authentication & Limits](/authentication).

## Ship it

You never built the hard parts: trader attribution through relays and solvers, candle continuity across launchpad migrations, a registry of \~90 launchpads, wash and sniper detection, USD conversion, icon hosting. That is the point: the panes above are the product, the pipeline behind them is ours.

<CardGroup cols={2}>
  <Card title="Get a key" icon="key" href="https://serialized.xyz/signup">Create an account, add a card to unlock 100,000 free credits, and create your key in the portal.</Card>
  <Card title="Migrate an existing terminal" icon="arrow-right-arrow-left" href="/migrate">Endpoint-by-endpoint maps from Mobula, Codex and Birdeye.</Card>
  <Card title="Streams reference" icon="tower-broadcast" href="/streams/overview">Auth, limits, reconnect semantics, billing.</Card>
  <Card title="Conventions" icon="ruler" href="/conventions">Envelope, chain ids, null semantics, timestamps.</Card>
</CardGroup>


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