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.