09.28.2026

How to Design Wallet History APIs for Trading-Grade Web3 Wallets and Dashboards

How to Design Wallet History APIs for Trading-Grade Web3 Wallets and Dashboards

A blueprint for trading-grade wallet history APIs: one merged timeline of transfers, calls, NFTs and prices, cursor pagination, and real-time updates.

How to Design Wallet History APIs for Trading-Grade Web3 Wallets and Dashboards

By the end of this tutorial, you’ll have a concrete blueprint for a trading‑grade wallet history API that:

  • Merges on‑chain transactions, internal calls, token transfers, NFTs, and prices into a single timeline
  • Uses cursor‑based pagination that doesn’t break when new blocks land
  • Streams real‑time updates without running your own indexers
  • Leverages Codex’s enriched wallet and event feeds instead of parsing raw chain data

This is a hands‑on, implementation‑first guide for engineering and product teams building Web3 wallets, trading dashboards, and portfolio apps.

If you want a deeper conceptual overview of requirements (PnL, trace APIs, token metadata, etc.), see the related in‑depth guide: Wallet Transaction History and Trace Transaction APIs: Data Requirements for Web3 Products.

Prerequisites

Before you start implementing, you should have:

  • Technical stack

    • Backend in Node.js, Python, Go, or similar
    • A database (Postgres, MySQL, or a document store) for local caching and user‑specific state
    • HTTP client and WebSocket client libraries
  • Codex access

    • A Codex API key (sign up at codex.io)
    • Ability to make GraphQL requests (Codex exposes an all‑in‑one GraphQL‑style API)
  • Product requirements defined

    • Which chains you support today and in the next 6–12 months
    • Whether you need cross‑chain timelines per user
    • Whether you show PnL, cost basis, and performance charts in the UI

1. Define Your Wallet History Requirements as a Merge Problem

A wallet history is not a single endpoint. It is a merge layer that combines multiple data sources into one ordered timeline.

Decide what “history” means for your product

At minimum, trading‑grade timelines should include:

  • Native transfers (e.g., ETH, SOL)
  • Token transfers (ERC‑20, SPL, etc.)
  • Swaps and DEX trades
  • NFT mints, transfers, sales
  • Protocol interactions (staking, bridging, lending)
  • Fiat on/off‑ramp activity (where applicable)

Codex’s wallet and event APIs already expose these as normalized objects with:

  • Token metadata (name, symbol, decimals, logos)
  • Price at time of trade (USD and native)
  • Event types (swap, mint, burn, transfer, etc.)
  • Resolved identity and labels

Actionable design:

Create a unified internal event type that everything maps to, for example:

// TypeScript example: your internal wallet event schema

export type WalletEventType =
  | "transfer_in"
  | "transfer_out"
  | "swap"
  | "nft_trade"
  | "bridge"
  | "stake"
  | "unstake"
  | "airdrop";

export interface WalletHistoryEvent {
  id: string;                 // opaque ID from provider + your suffix
  walletAddress: string;
  chainId: string;            // e.g. 'eth-mainnet', 'solana-mainnet'
  timestamp: string;          // ISO-8601
  blockNumber: string;
  txHash: string;
  eventType: WalletEventType;

  // Financials
  amountNative?: string;
  amountUsd?: string;
  feeNative?: string;
  feeUsd?: string;

  // Assets
  tokenIn?: TokenRef;
  tokenOut?: TokenRef;
  nfts?: NftRef[];

  // Enrichment
  priceAtExecutionUsd?: string;
  realizedPnlUsd?: string;
  counterpartyLabel?: string;
  protocolLabel?: string;
  sourcePlatform?: string;    // e.g. 'Uniswap', 'Magic Eden', 'Polymarket'
}

export interface TokenRef {
  address: string;
  chainId: string;
  symbol: string;
  decimals: number;
}

export interface NftRef {
  contractAddress: string;
  tokenId: string;
  collectionName?: string;
}

Common failure: starting with your provider’s raw response schema and leaking it into your API. That makes it hard to switch vendors or add new event types. Instead, treat Codex (or any provider) as a source that you normalize into your own WalletHistoryEvent shape.

2. Choose Cursor‑Based Pagination From Day One

For trading‑grade UX, pagination is not a detail — it’s a contract.

Offset pagination (page=2&limit=50) breaks when:

  • New blocks insert events at the top of the feed
  • Chain reorgs reorder or remove transactions
  • Your filters change (e.g., hiding spam tokens)

Use opaque cursors in your API

Codex’s event queries already use cursor pagination. Cursors are opaque tokens that encode position in the stream and survive inserts.

Design your public API like this:

GET /v1/wallets/{walletAddress}/history?cursor=opaqueCursor123&limit=50

Example JSON response:

{
  "items": [
    { "id": "evt_1", "eventType": "swap", "timestamp": "2026-09-26T10:00:00Z" },
    { "id": "evt_2", "eventType": "transfer_in", "timestamp": "2026-09-26T09:59:10Z" }
  ],
  "nextCursor": "opaqueCursor124",
  "prevCursor": "opaqueCursor122",
  "hasMore": true
}

You can directly reuse Codex’s cursor in most cases. If you want abstraction from vendor specifics:

  • Store providerCursor in your DB
  • Issue your own nextCursor tokens (e.g., signed JWT or UUID mapping to a row that holds the provider cursor)

Common failure: mixing page‑based and cursor‑based pagination across endpoints. Pick cursor only for all wallet history APIs, including NFT activity and cross‑chain views.

3. Model the Query Layer: One Endpoint, Multiple Sources

Your frontend should call one wallet history endpoint. Internally, that endpoint can fan out to different Codex queries depending on filters and time ranges.

Example: backend route definition

// Pseudo-code Express/Node route

app.get("/v1/wallets/:address/history", async (req, res) => {
  const { address } = req.params;
  const { cursor, limit = 50, chains, types } = req.query;

  const result = await walletHistoryService.fetchTimeline({
    address,
    cursor: cursor as string | undefined,
    limit: Number(limit),
    chains: (chains as string | undefined)?.split(","),
    types: (types as string | undefined)?.split(","),
  });

  res.json(result);
});

Inside walletHistoryService.fetchTimeline

Use Codex queries such as:

  • walletEvents (or a filtered token/NFT event query)
  • detailedWalletStats for high‑level info and PnL windows
  • walletChart for performance charts

A simplified GraphQL request to Codex might look like:

query WalletEvents($address: String!, $cursor: String, $limit: Int) {
  walletEvents(
    address: $address
    cursor: $cursor
    limit: $limit
  ) {
    items {
      id
      chainId
      timestamp
      txHash
      eventType
      amountNative
      amountUsd
      tokenIn { address symbol decimals }
      tokenOut { address symbol decimals }
      priceAtExecutionUsd
      realizedPnlUsd
      protocolLabel
    }
    nextCursor
  }
}

Map each Codex item into your internal WalletHistoryEvent type. For example, map Codex’s eventType to your own WalletEventType enum.

Common failure: exposing multiple endpoints (e.g., /transactions, /swaps, /nfts) and merging on the client. That increases network chatter, complicates pagination, and makes vendor switching harder.

4. Normalize and Deduplicate Events Across Chains

Trading‑grade timelines must be consistent across 80+ networks, not just Ethereum.

Codex gives you a head start here: it already normalizes token metadata and event types across 80+ chains and 2B+ wallets. But you still need a thin normalization layer.

Deduplication strategy

You may see the same logical event via:

  • A transaction trace
  • A token transfer
  • An NFT market event

Define a stable deduplication key, for example:

const dedupeKey = `${chainId}:${txHash}:${logIndex || 0}:${subType || ""}`;

Store processed keys in a local cache (Redis) or DB table to avoid double‑rendering the same event.

Cross‑chain representation

Normalize chain IDs to your own canonical list:

// Example mapping

const CHAIN_MAP: Record<string, string> = {
  "1": "eth-mainnet",
  "137": "polygon-mainnet",
  "solana-mainnet": "solana-mainnet",
};

Every event you expose should have a chainId from this list. That allows:

  • Cross‑chain filters in your UI
  • Efficient per‑chain caching strategies

Common failure: using raw chain identifiers from multiple vendors (e.g., 1, ethereum_mainnet, ETHEREUM). This makes filters and analytics buggy and brittle.

5. Attach Prices, PnL, and Performance Windows

Trading apps live and die on PnL correctness. Users expect:

  • Cost basis per position
  • Realized profit and loss
  • Performance over 1D / 1W / 1M / 1Y windows

Codex exposes this out of the box via endpoints like detailedWalletStats and walletChart, with:

  • PnL windows (1D/1W/1M/1Y)
  • Chart data at resolutions like 60, 240, 1D, 7D
  • Metrics such as volumeUsd, realizedProfitUsd, swaps

Example: enrich history with stats

Your wallet history endpoint can optionally include a summary block:

{
  "summary": {
    "wallet": "0xabc...",
    "totalVolumeUsd1D": "15023.45",
    "realizedProfitUsd1W": "-204.18",
    "tradeCount1M": 132
  },
  "events": [ /* ...your events... */ ]
}

Fetch summary from Codex’s wallet stats / chart endpoints in parallel with the timeline events, and stitch the results before returning.

Common failure: computing PnL in‑house from raw trades and prices for every user. This is expensive and error‑prone. Use Codex’s enriched stats as the source of truth and only add thin product‑specific logic on top.

6. Split Historical Backfill From Real‑Time Tail Updates

High‑traffic wallets and dashboards need instant initial load plus near real‑time updates.

Codex recommends using:

  • Queries for one‑time fetches and historical backfill
  • Subscriptions for live streaming updates
  • Webhooks for backend notifications and background jobs

Architecture pattern

  1. Initial page load

    • Client calls your /wallets/:address/history endpoint
    • Backend fetches from Codex (GraphQL query) and returns first page + cursors
  2. Live updates

    • Backend subscribes to Codex wallet/event subscriptions per wallet or per address pattern
    • On new event, backend:
      • Normalizes to WalletHistoryEvent
      • Updates your cache/DB
      • Pushes via WebSocket or Server‑Sent Events to clients subscribed to that wallet
  3. Webhooks for heavy work

    • Set up Codex webhooks to notify your backend of large events (e.g., high‑value trades, whales)
    • Trigger background processing (notifications, risk checks, etc.)

Common failure: polling the provider every few seconds for each wallet. This causes rate‑limit issues and unnecessary load, and still produces slower UX than subscriptions + webhooks.

7. Design Around Vendor Risk With an Abstraction Layer

Vendor continuity is now a real requirement. When SimpleHash shut down its Token API, The Graph launched a Token API specifically to preserve continuity for wallets and explorers. The lesson: assume providers can change.

Introduce an internal provider interface

Define an interface that your application talks to, and implement Codex (and optionally a fallback provider) behind it.

export interface WalletHistoryProvider {
  fetchEvents(params: {
    address: string;
    cursor?: string;
    limit: number;
  }): Promise<{ events: WalletHistoryEvent[]; nextCursor?: string }>;

  fetchStats(address: string): Promise<WalletStatsSummary>;
}

Codex implementation:

export class CodexWalletHistoryProvider implements WalletHistoryProvider {
  constructor(private readonly codexClient: CodexGraphQLClient) {}

  async fetchEvents(params: { address: string; cursor?: string; limit: number }) {
    const { address, cursor, limit } = params;
    const response = await this.codexClient.query(WALLET_EVENTS_QUERY, {
      address,
      cursor,
      limit,
    });

    const events = response.walletEvents.items.map(mapCodexToWalletEvent);

    return {
      events,
      nextCursor: response.walletEvents.nextCursor,
    };
  }

  async fetchStats(address: string): Promise<WalletStatsSummary> {
    // call Codex detailedWalletStats + walletChart here
  }
}

Your HTTP handlers talk only to WalletHistoryProvider, not to Codex directly.

Common failure: tightly coupling your REST or GraphQL schema to one vendor’s response format. That makes migration painful and slows you down when adding new features.

8. Implement Caching and Latency Budgets for Trading‑Grade UX

Trading‑adjacent products care deeply about sub‑second latency and high uptime. Codex highlights sub‑200ms hot‑cache reads for holdings and balances and targets near‑99.99% uptime for trading‑grade data.

Practical caching strategies

  • Short‑lived in‑memory cache (e.g., 1–5 seconds) for wallet history pages for extremely hot wallets
  • Per‑cursor caching – cache results keyed by wallet + cursor so the “Back” button and infinite scroll feel instant
  • Chain‑aware TTLs – shorter TTLs for fast chains (e.g., Solana) where new events land quickly; longer TTLs for slower networks

Example cache keying:

const cacheKey = `walletHistory:${walletAddress}:${cursor || "root"}:${limit}`;

Latency budget

Aim for:

  • 200–400 ms backend processing time for cached data
  • <1s cold path including Codex query + normalization + network latency

Common failure: trying to cache per‑transaction forever. Wallet timelines are dynamic — filters, labels, and PnL can change as prices update. Keep caches small and short‑lived; rely on Codex’s own indexing and enrichment for most heavy lifting.

9. Add Test Coverage and Edge‑Case Handling

High‑traffic wallets and dashboards need predictable behavior in edge cases.

Edge cases to cover

  • Wallet with zero activity
  • Wallet with very sparse activity over years
  • Wallet with extremely high activity (power user, market maker)
  • Chains where Codex backfill is still in progress (Codex exposes backfill state for some aggregates)
  • Filters that hide all events on a page (Codex notes that cursor pages can be empty due to filtering)

Example tests

  • Cursor stability test:

    • Fetch a page with cursor A
    • Insert a new event at the top via a fixture or mock
    • Fetch again with cursor A and assert the same page contents (no duplication or skipping)
  • Merge ordering test:

    • Simulate events across eth-mainnet and polygon-mainnet
    • Assert that the combined timeline is ordered strictly by timestamp and blockNumber

Common failure: ignoring the “empty page” case with filtered feeds, leading to broken infinite scroll. Always respect provider cursors even when a page has zero visible events.

Designing a trading‑grade wallet history API is fundamentally about how you merge and present enriched on‑chain data, not how you parse raw logs. By pairing a clean internal schema, cursor‑based pagination, and Codex’s wallet and event APIs, you can ship reliable, high‑performance wallet timelines without building your own indexers or maintaining fragile blockchain ETL.

FAQ

Continuereading

10,000 free requests / mo$1 one-time signup feeStart building in minutes

Ready to have thefastest onchain data?

Get real-time crypto and prediction market data within minutes. Start with 10,000 requests per month for free.