← Back
import { useState, useEffect, useCallback } from 'react';

// --- Types ---

export interface VolumeEntry {
  dt: string;
  volume: number;
  activeUsers: number;
  rank: string;
}

export interface BuilderTrade {
  id: string;
  tradeType: 'MAKER' | 'TAKER';
  market: string;
  assetId: string;
  side: 'BUY' | 'SELL';
  size: string;
  sizeUsdc: string;
  price: string;
  fee: string;
  feeUsdc: string;
  builderFee: string;
  outcome: string;
  maker: string;
  owner: string;
  transactionHash: string;
  matchTime: string;
  createdAt: string;
}

export interface RankEntry {
  rank: string;
  builder: string;
  builderCode: string;
  volume: number;
  activeUsers: number;
  verified: boolean;
}

export interface AnalyticsTotals {
  totalVolume: number;
  totalFees: number;      // platform fee (Polymarket) — sum of feeUsdc
  myBuilderFee: number;   // our builder revenue — sum of builderFee
  makerFee: number;       // builder revenue from MAKER fills
  takerFee: number;       // builder revenue from TAKER fills
  totalTrades: number;
  activeUsers: number;
  rank: string;
}

// --- Hook ---

export interface PayoutEntry {
  amount: number;
  timestamp: string;
  hash: string;
  token: string;
}

export function useAnalytics() {
  const [volume, setVolume] = useState<VolumeEntry[]>([]);
  const [trades, setTrades] = useState<BuilderTrade[]>([]);
  const [rank, setRank] = useState<RankEntry | null>(null);
  const [paidTotal, setPaidTotal] = useState(0);
  const [payouts, setPayouts] = useState<PayoutEntry[]>([]);
  const [totals, setTotals] = useState<AnalyticsTotals>({
    totalVolume: 0, totalFees: 0, myBuilderFee: 0, makerFee: 0, takerFee: 0,
    totalTrades: 0, activeUsers: 0, rank: '-',
  });
  const [loading, setLoading] = useState(true);

  const fetchVolume = useCallback(async () => {
    try {
      const res = await fetch('/api/analytics/volume?period=ALL');
      const d = await res.json();
      if (d.success) setVolume(d.data ?? []);
    } catch { /* ignore */ }
  }, []);

  const fetchTrades = useCallback(async () => {
    try {
      // Walk all CLOB pages so totals stay accurate past one page (limit 300/page).
      // CLOB signals end with next_cursor 'LTE=' (base64 "-1"). Cap at 50 pages (15K trades).
      const all: BuilderTrade[] = [];
      let cursor: string | undefined;
      for (let page = 0; page < 50; page++) {
        const qs = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';
        const res = await fetch(`/api/analytics/trades${qs}`);
        const d = await res.json();
        if (!d.success) break;
        const items: BuilderTrade[] = d.data?.data ?? [];
        all.push(...items);
        const next: string | undefined = d.data?.next_cursor;
        if (!next || next === 'LTE=' || items.length === 0) break;
        cursor = next;
      }
      setTrades(all);

      // Compute totals from ALL trades (leaderboard omits us until top-50).
      // totalFees = platform fee (feeUsdc); myBuilderFee = our revenue (builderFee).
      const totalFees = all.reduce((sum, t) => sum + (parseFloat(t.feeUsdc) || 0), 0);
      const myBuilderFee = all.reduce((sum, t) => sum + (parseFloat(t.builderFee) || 0), 0);
      const makerFee = all.reduce((sum, t) => sum + (t.tradeType === 'MAKER' ? parseFloat(t.builderFee) || 0 : 0), 0);
      const takerFee = all.reduce((sum, t) => sum + (t.tradeType === 'TAKER' ? parseFloat(t.builderFee) || 0 : 0), 0);
      const totalVolume = all.reduce((sum, t) => sum + (parseFloat(t.sizeUsdc) || 0), 0);
      const activeUsers = new Set(all.map(t => t.owner).filter(Boolean)).size;
      setTotals(prev => ({
        ...prev, totalFees, myBuilderFee, makerFee, takerFee, totalVolume, activeUsers, totalTrades: all.length,
      }));
    } catch { /* ignore */ }
  }, []);

  const fetchRank = useCallback(async () => {
    try {
      const res = await fetch('/api/analytics/rank?period=ALL');
      const d = await res.json();
      if (d.success && d.data) {
        const entry = d.data as RankEntry;
        setRank(entry);
        setTotals(prev => ({
          ...prev,
          totalVolume: entry.volume ?? 0,
          activeUsers: entry.activeUsers ?? 0,
          rank: entry.rank ?? '-',
        }));
      }
    } catch { /* ignore */ }
  }, []);

  const fetchPayouts = useCallback(async () => {
    try {
      const res = await fetch('/api/analytics/payouts');
      const d = await res.json();
      if (d.success && d.data) {
        setPaidTotal(d.data.paidTotal ?? 0);
        setPayouts(d.data.payouts ?? []);
      }
    } catch { /* ignore */ }
  }, []);

  const refresh = useCallback(async () => {
    setLoading(true);
    await Promise.all([fetchVolume(), fetchTrades(), fetchRank(), fetchPayouts()]);
    setLoading(false);
  }, [fetchVolume, fetchTrades, fetchRank, fetchPayouts]);

  useEffect(() => {
    const t = setTimeout(refresh, 0);
    const interval = setInterval(refresh, 5 * 60_000); // 5 min auto-refresh
    return () => { clearTimeout(t); clearInterval(interval); };
  }, [refresh]);

  return { volume, trades, rank, totals, paidTotal, payouts, loading, refresh };
}

📜 Git History

6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...