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
providerCursorin your DB - Issue your own
nextCursortokens (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)detailedWalletStatsfor high‑level info and PnL windowswalletChartfor 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
-
Initial page load
- Client calls your
/wallets/:address/historyendpoint - Backend fetches from Codex (GraphQL query) and returns first page + cursors
- Client calls your
-
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
- Normalizes to
-
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 + cursorso 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
Aand assert the same page contents (no duplication or skipping)
- Fetch a page with cursor
-
Merge ordering test:
- Simulate events across
eth-mainnetandpolygon-mainnet - Assert that the combined timeline is ordered strictly by
timestampandblockNumber
- Simulate events across
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.



