> ## 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 Wallet Tracker

> Find wallets worth following, identify who is behind them, and render a full trader dashboard: equity curve, realized and unrealized PnL, win rate, open and closed positions, and a live activity feed - then scale it to a fleet of tracked wallets with batches.

Copy-trading feeds, smart-money dashboards, KOL trackers: they are all the same product. Find wallets worth following, profile them, render their performance, and watch what they do next. This guide builds each screen of that product on the wallet endpoints, EVM and Solana with the same shapes throughout.

| Tracker screen | Powered by |
| - | - |
| Discover wallets worth tracking | [`GET /v1/token/top-traders`](/endpoints/token-top-traders) · [`GET /v1/pool/top-traders`](/endpoints/pool-top-traders) |
| Who is behind the address | [`GET /v1/wallet/profile`](/endpoints/wallet-profile) + [`GET /v1/wallet/funding`](/endpoints/wallet-funding) |
| Headline tiles (value, PnL, win rate) | [`GET /v1/wallet/positions`](/endpoints/wallet-positions) `meta.summary` + [`GET /v1/wallet/pnl`](/endpoints/wallet-pnl) |
| Equity curve | [`GET /v1/wallet/equity/history`](/endpoints/wallet-equity-history) |
| Open positions table | [`GET /v1/wallet/positions`](/endpoints/wallet-positions) |
| Closed positions (realized track record) | [`GET /v1/wallet/closed-positions`](/endpoints/wallet-closed-positions) |
| Activity feed | [`GET /v1/wallet/swaps`](/endpoints/wallet-swaps) |
| Per-token trade history | [`GET /v1/wallet/trades`](/endpoints/wallet-trades) · [`/v1/wallet/transfers`](/endpoints/wallet-transfers) |
| A fleet of tracked wallets | the [batch twins](/endpoints/wallet-positions-batch), up to 20 wallets per call |

<Note>Setup is the same as the [terminal guide](/guides/build-a-trading-terminal#ground-rules): one `Authorization` header, `{ data, meta }` envelopes, `chain` = `evm:<id>` or `solana`. The `sd()` helper below is the one defined there.</Note>

## Find wallets worth tracking

The discovery entry point is a token, not a wallet: every trader you want to follow is printing on some market. Two rankings, same shape:

* [`GET /v1/token/top-traders`](/endpoints/token-top-traders) with `timeframe=all` is the **lifetime leaderboard** of a token: realized PnL, win rate over closed positions, current bag and its unrealized PnL, `isDev` / `isSniper` / `isProTrader` badges on every row.
* The same endpoint with `timeframe=1h` → `30d`, or [`GET /v1/pool/top-traders`](/endpoints/pool-top-traders) for a single market, ranks by **net flow over the window**: who is printing right now.

```typescript discover.ts theme={null}
const { data } = await sd<Trader[]>('/token/top-traders',
  { chain: 'solana', address: mint, timeframe: 'all', limit: 20 });

const candidates = data.filter((t) =>
  t.winRatePct !== null && t.winRatePct > 60 && t.realizedPnlUsd > 10_000 && !t.isSniper);
```

A third source is your own tape: every row of [`/v1/token/trades`](/endpoints/trades-tape) and the [`trades` stream](/streams/trades-stream) carries `maker` with the same badges, so "track this wallet" belongs on every trade row of your terminal.

## Who is behind the address

Two calls turn an address into a person, or a red flag:

* [`GET /v1/wallet/profile`](/endpoints/wallet-profile) resolves the identity: display name, ENS / Basename / .sol, avatar, bio, social accounts with follower counts, the pro-trader flag, and `linkedWallets`, other addresses of the same person from our identity graph of 7.6M+ labeled wallets. A KOL tracker is this one endpoint.
* [`GET /v1/wallet/funding`](/endpoints/wallet-funding) answers where the wallet's first funds came from: funder address, timestamp, amount, and `funderTag` when the funder is a known entity (exchange, bridge). A fresh wallet funded by another tracked wallet minutes before a launch is a signal; the insider and sybil heuristics start here.

```typescript theme={null}
const [{ data: p }, { data: f }] = await Promise.all([
  sd('/wallet/profile', { address: wallet }),
  sd('/wallet/funding', { chain, wallet }),
]);
// p.profile.displayName, p.profile.socials.twitter, p.profile.linkedWallets
// f.funder, f.fundedAt, f.funderTag ("Binance", a bridge such as "Relay.link", or null)
```

## The dashboard

Four calls render the whole trader page, and they are independent: fire them together and render each pane as its data lands, so one slow pane never blanks the page.

```typescript dashboard.ts theme={null}
const panes = {
  positions: sd('/wallet/positions', { chain, wallet, includeNative: true, includeStables: true }),
  closed:    sd('/wallet/closed-positions', { chain, wallet, sort: 'pnl', limit: 20 }),
  pnl:       sd('/wallet/pnl', { chain: chain === 'solana' ? 'solana' : undefined, wallet, period: '30d' }),
  equity:    sd('/wallet/equity/history', { chain, wallet }),
};
for (const [pane, p] of Object.entries(panes))
  p.then((r) => render(pane, r), (e) => scheduleRetry(pane, e));
```

A `503` on one pane carries `Retry-After`: the snapshot behind it is being recomputed, so retry that pane alone after the delay while the rest of the page is already on screen. `5xx` responses are never billed.

* **Headline tiles**: `positions.meta.summary` carries the wallet-level totals (`totalValueUsd`, `realizedPnlUsd`, `unrealizedPnlUsd`, `totalPnlUsd`, `positions`), computed over every position whatever the page. With `includeNative` and `includeStables` the total is the real account value, native coin and stablecoins read on-chain at request time.
* **Win rate and cadence**: `pnl.data.summary` has `realizedPnlUsd`, `winRate`, `trades`, `wins`, `losses`; `pnl.data.history` is the realized PnL series for the bar chart, daily buckets on `30d` and `all`, hourly on `1d` and `7d`. Omit `chain` to combine every EVM chain in one figure.
* **Equity curve**: `equity.data.points` is the `{ at, valueUsd }` series behind the portfolio chart, `currentValueUsd` the latest mark.
* **Open positions**: every row has the current bag (`amount`, `amountUsd`), `entryPriceUsd` vs `currentPriceUsd`, realized + unrealized PnL, and `holdingSince`, which resets when the wallet fully exits and re-enters: the honest "holding for 3d" display.
* **Closed positions**: the realized track record, sortable by PnL, with `realizedPnlPercent` (realized over total bought) per token. Across positions and closed-positions the realized PnL of a token is counted exactly once, so the two tables never double-count.

## The activity feed

[`GET /v1/wallet/swaps`](/endpoints/wallet-swaps) is built for the feed: **one row per transaction**, whatever the route did. `sent` is what the swap took from the wallet and `received` what it delivered, read from end to end: a USELESS to USDC sale routed through SOL reads as USELESS sent, USDC received, and the assets the route only passes through do not appear. Every asset carries its `symbol` and `decimals`, `route` lists every leg with its pool and venue, and on Solana each swap carries `router` (Jupiter, OKX DEX, DFlow, Titan, `null` for a direct swap), so a row renders as "swapped 14.55 USELESS for 3.42 USDC via DFlow" with no extra lookup.

There is no wallet-scoped stream channel, and the pagination is designed so you do not need one: cursors are exact keyset, they never skip or repeat a transaction. The lossless live pattern is a short poll that walks until it meets a row it has seen:

```typescript activity-poll.ts theme={null}
const seen = new Set<string>();

async function pollActivity(chain: string, wallet: string, onRow: (r: Swap) => void) {
  let cursor: string | undefined;
  while (true) {
    const { data, meta } = await sd<Swap[]>('/wallet/swaps', { chain, wallet, limit: 25, cursor });
    const fresh = data.filter((r) => !seen.has(r.id));
    fresh.forEach((r) => { seen.add(r.id); onRow(r); });
    if (fresh.length < data.length || !meta.hasMore) return;   // met a known row: caught up
    cursor = meta.nextCursor;                                  // burst bigger than a page: keep walking
  }
}
setInterval(() => pollActivity(chain, wallet, prependRow), 15_000);
```

For a per-token tape of the wallet (every fill, not netted per transaction), use [`GET /v1/wallet/trades`](/endpoints/wallet-trades): multihop-deduplicated so volumes are never doubled, routing hops excluded by default, `isWash` flagged on Solana. [`/v1/wallet/transfers`](/endpoints/wallet-transfers) adds deposits and withdrawals with counterparties, which is how a feed distinguishes "sold" from "moved to another wallet".

## Track a fleet

A tracker follows N wallets, and N single calls do not scale. Every wallet endpoint has a `POST` batch twin taking up to 20 `{chain, wallet}` items, chains mixed freely (omit `chain` on a hex address to combine all its EVM chains), per-item error slots so one wallet never fails the others, billed per wallet: [positions](/endpoints/wallet-positions-batch), [PnL](/endpoints/wallet-pnl-batch), [closed positions](/endpoints/wallet-closed-positions-batch), [profile](/endpoints/wallet-profile-batch), [trades](/endpoints/wallet-trades-batch), [swaps](/endpoints/wallet-swaps-batch), [funding](/endpoints/wallet-funding-batch).

The fleet overview (one leaderboard row per tracked wallet) is two batch calls. A positions slot is `{ data, summary, hasMore }`, exactly the body of the single GET; a PnL slot is `{ data }`, and `period` applies to the whole call:

```typescript fleet.ts theme={null}
const items = fleet.map(({ chain, wallet }) => ({ chain, wallet }));
const [positions, pnl] = await Promise.all([
  sd2('/wallet/positions', { items, limit: 1 }),           // POST twin; summary covers ALL positions
  sd2('/wallet/pnl', { items, period: '7d' }),
]);
const rows = fleet.map((w, i) => ({
  ...w,
  valueUsd: positions.data[i].summary?.totalValueUsd ?? null,
  pnl7dUsd: pnl.data[i].data?.summary?.realizedPnlUsd ?? null,
}));
```

Refresh it on an interval and sort by 7d PnL: the fleet ranks itself.

## The copy-trade check

The question a copy-trader asks is not "what does this wallet hold", it is "is this wallet still in the token I copied". `tokens=` scopes both position endpoints to specific tokens, one call, no paging:

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

A row back means they still hold, with the live unrealized PnL of their bag. Nothing back: check [`/v1/wallet/closed-positions?tokens=MINT`](/endpoints/wallet-closed-positions) for the exit and its realized PnL. Between the two, the answer is always exactly one call away.

## What a tracker costs

| Action | Calls | Credits |
| - | - | - |
| Discover (token leaderboard) | 1 × `top-traders` | 2 |
| Vet a wallet (profile + funding) | 2 calls | 2 |
| Dashboard open (positions, closed, PnL, equity) | 4 calls | 6 |
| Activity poll | 1 × `swaps` per tick | 1 |
| Fleet overview, 20 wallets | 2 batch calls | 60 |
| Copy-trade check | 1 × `positions?tokens=` | 2 |

A 15s activity poll is 4 credits per minute per watched wallet; widen the interval for the fleet, keep it tight for the wallet on screen. Only `2xx` responses are billed. Plans and the full grid are on [Authentication & Limits](/authentication).

## Ship it

The heavy machinery came with the endpoints: PnL folds over every trade of the wallet, multihop dedup so volumes are never doubled, route netting for readable feeds, an identity graph of 7.6M+ wallets, funding lineage, exact keyset pagination. Your tracker is the UI on top.

<CardGroup cols={2}>
  <Card title="Build a Trading Terminal" icon="desktop" href="/guides/build-a-trading-terminal">The token side of the product: discovery, chart, tape, panels.</Card>
  <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>
</CardGroup>


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