Fomo trader data in TypeScript: a quickstart with fetch
A typed TypeScript client for fomo trader data: leaderboards, trader lookups and wallet resolution with nothing but fetch.
This quickstart builds a small typed client with nothing but fetch, so it runs in Node 18+, Deno, Bun and edge runtimes alike.
1. Types for the fields you use
You do not need to type every field. Type the ones you read, and remember that unknown values are null:
type Wallet = { chain: string; address: string; status: "verified" | "likely" };
type Trader = {
handle: string;
displayName: string | null;
pnl: { "24h": number | null; "7d": number | null; "30d": number | null; all: number | null };
volumeUsd: number | null;
walletList: Wallet[];
walletsVerified: boolean;
checked: boolean;
};
type Ranked = Trader & { rank: number; previousRank: number | null; pnlUsd: number | null };
type Leaderboard = { window: string; capturedAt: string | null; count: number; traders: Ranked[] };
2. A tiny client
const API = "https://provadata.com";
const KEY = process.env.API_KEY!;
class ApiError extends Error {
constructor(public status: number, public code: string, public retryAfter?: number) {
super(`${status} ${code}`);
}
}
async function call<T>(path: string): Promise<{ data: T; status: number; remaining: number }> {
const res = await fetch(API + path, { headers: { Authorization: `Bearer ${KEY}` } });
const body = await res.json();
if (!res.ok) {
throw new ApiError(res.status, body.error, Number(res.headers.get("retry-after")) || undefined);
}
return { data: body as T, status: res.status, remaining: Number(res.headers.get("x-credits-remaining")) };
}
Errors always look like {"error": "code", "message": "…"}, so switching on code is safe.
3. Leaderboard
const { data: board, remaining } = await call<Leaderboard>("/v2/leaderboard/7d?limit=20");
for (const t of board.traders) {
const climbed = t.previousRank === null ? "new" : t.previousRank - t.rank;
console.log(t.rank, t.handle, t.pnlUsd, climbed, t.walletsVerified);
}
console.log("credits left:", remaining);
4. One trader, with 202 handled
async function trader(handle: string): Promise<Trader | null> {
for (let i = 0; i < 4; i++) {
try {
const { data, status } = await call<Trader & { retryAfterSeconds?: number }>(`/v2/users/${handle}`);
if (status === 202) {
await new Promise((r) => setTimeout(r, (data.retryAfterSeconds ?? 15) * 1000));
continue;
}
return data;
} catch (e) {
if (e instanceof ApiError && e.status === 404) return null;
throw e;
}
}
return null;
}
const t = await trader("halvard");
const proven = t?.walletList.filter((w) => w.status === "verified") ?? [];
5. Resolve a wallet
type Owner = { address: string; chain: string; status: string; conflict: boolean; trader: Trader };
const { data: owner } = await call<Owner>("/v2/wallets/7xKq…9fQe");
if (owner.status === "verified" && !owner.conflict) {
console.log("owned by", owner.trader.handle);
}
Costs
Leaderboard 100 credits, trader and wallet 500, batches 250 per item found. A 202, 404 or 503 is free. See credits and retries for production patterns.