Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f5e99dd
feat(competition): per-agent MCP trade count + USD volume
biwasxyz May 11, 2026
025550a
feat(agents): enrich /agents SSR with MCP submission count + USD volume
biwasxyz May 11, 2026
29c1353
feat(competition): extend AgentTradeSummary with latestTradeAt
biwasxyz May 11, 2026
3ac65de
feat(agents): thread mcpLatestTradeAt through SSR data
biwasxyz May 11, 2026
e89342d
feat(agents): MCP Trades + Volume + Latest Trade columns on /agents
biwasxyz May 11, 2026
3341ea7
fix(competition): log Tenero failures + send explicit User-Agent
biwasxyz May 11, 2026
c354ebb
fix(competition): route Tenero diagnostics through Logger (lint)
biwasxyz May 11, 2026
1018616
revert: drop volume.ts + /agents enrichment, switch to /leaderboard page
biwasxyz May 11, 2026
c4d7c30
feat(leaderboard): /leaderboard page (server-rendered, D1-only)
biwasxyz May 11, 2026
85c5a77
feat(leaderboard): LeaderboardClient with browser-side Tenero + local…
biwasxyz May 11, 2026
3ad63e6
feat(navbar): Leaderboard link in desktop + mobile menus
biwasxyz May 11, 2026
7450e0f
copy(leaderboard): tighten subtitle + metadata
biwasxyz May 11, 2026
38fa71a
feat(leaderboard): agent avatars in desktop + mobile rows
biwasxyz May 11, 2026
a7cce1b
style(navbar): de-button desktop nav links — text-only with hover
biwasxyz May 11, 2026
56363d1
refactor(leaderboard): drop KV agent-list cache, read display data di…
biwasxyz May 12, 2026
151f472
perf(leaderboard): single LEFT JOIN query + 60s ISR window
biwasxyz May 12, 2026
ff9c19b
fix(leaderboard): use async-mode getCloudflareContext so revalidate=6…
biwasxyz May 12, 2026
9c744a0
feat(tenero): typed fetch wrapper modeled on stacks-api-fetch
biwasxyz May 12, 2026
8017e59
feat(tenero): fetchTokenPriceUsd built on the fetch wrapper
biwasxyz May 12, 2026
3728731
feat(tenero): KV cache helpers for tenero:price:{tokenId}
biwasxyz May 12, 2026
b779108
chore(tenero): barrel export for lib/external/tenero
biwasxyz May 12, 2026
88a3bc5
feat(scheduler): SchedulerDO + alarm for Tenero price refresh
biwasxyz May 12, 2026
2d55c71
types(env): add SCHEDULER + optional TENERO_API_KEY bindings
biwasxyz May 12, 2026
37c816a
config(wrangler): register SchedulerDO binding + v1 migration
biwasxyz May 12, 2026
c431504
feat(worker): export SchedulerDO so the runtime finds the class
biwasxyz May 12, 2026
0226d72
feat(leaderboard): SSR volumeUsd from KV-cached Tenero prices
biwasxyz May 12, 2026
5107255
refactor(leaderboard): drop browser Tenero fetch — presentational client
biwasxyz May 12, 2026
74c9ea7
feat(leaderboard): opportunistic SchedulerDO kick on SSR
biwasxyz May 12, 2026
ff1eca2
chore: rebuild to exercise versions upload + get branch preview URL
biwasxyz May 12, 2026
f05f13a
fix(scheduler): inline SchedulerDO in worker.ts to survive bundling
biwasxyz May 12, 2026
46e6bad
fix(scheduler): RPC-compatible typing for SCHEDULER binding
biwasxyz May 12, 2026
b2dd3e9
refactor(tenero): extract STATIC_TOKEN_IDS to shared module
biwasxyz May 12, 2026
9a99183
refactor(scheduler): extract runTeneroTask to lib/scheduler/
biwasxyz May 12, 2026
f7232b3
refactor(scheduler): apply #743 review feedback to SchedulerDO
biwasxyz May 12, 2026
c33602c
feat(api): GET /api/prices route for cached Tenero prices
biwasxyz May 12, 2026
922bddf
test(tenero): unit tests for prices + kv-cache helpers
biwasxyz May 12, 2026
da3227e
test(scheduler): unit tests for runTeneroTask
biwasxyz May 12, 2026
daf6d5e
fix(leaderboard): BigInt-safe SUM aggregate parse + correct scheduler…
biwasxyz May 12, 2026
dd54ec0
fix(balances): BigInt-parse sBTC satoshi string to preserve precision
biwasxyz May 12, 2026
6e5dcfe
docs(inbox): clarify UNIQUE-violation substring match in d1-dual-write
biwasxyz May 12, 2026
d72559e
fix(leaderboard): render dynamically for Cloudflare bindings
whoabuddy May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,15 @@ export default function Navbar() {

{[
{ href: "/agents", label: "Agent Network" },
{ href: "/leaderboard", label: "Leaderboard" },
{ href: "/activity", label: "Activity Feed" },
{ href: "/bounty", label: "Bounties" },
{ href: "/skills", label: "Skills" },
].map((link) => (
<Link
key={link.href}
href={link.href}
className="inline-flex items-center justify-center rounded-lg border border-white/15 bg-[rgba(30,30,30,0.8)] backdrop-blur-sm px-2.5 py-1.5 text-xs lg:px-4 lg:py-2 lg:text-sm font-medium text-white/80 transition-[background-color,border-color,color,transform] duration-200 hover:border-white/25 hover:bg-[rgba(45,45,45,0.85)] hover:text-white active:scale-[0.97]"
className="inline-flex items-center px-2 py-1 text-xs lg:px-3 lg:text-sm font-medium text-white/60 transition-colors duration-200 hover:text-white"
>
{link.label}
</Link>
Expand All @@ -173,6 +174,7 @@ export default function Navbar() {
>
{[
{ href: "/agents", label: "Agent Network" },
{ href: "/leaderboard", label: "Leaderboard" },
{ href: "/activity", label: "Activity Feed" },
{ href: "/bounty", label: "Bounties" },
{ href: "/skills", label: "Skills" },
Expand Down
217 changes: 217 additions & 0 deletions app/leaderboard/LeaderboardClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"use client";

import Link from "next/link";
import { generateName } from "@/lib/name-generator";
import { truncateAddress, formatRelativeTime } from "@/lib/utils";

export interface LeaderboardRow {
stxAddress: string;
btcAddress: string | null;
displayName: string | null;
bnsName: string | null;
erc8004AgentId: number | null;
tradeCount: number;
latestTradeAt: number;
/**
* USD volume computed server-side from KV-cached Tenero prices. Comes in
* as a plain number so this component stays presentational — no fetch,
* no localStorage, no useEffect.
*/
volumeUsd: number;
/**
* False if any token in the agent's volume couldn't be priced from KV
* (cold scheduler, paused, or token has no published price). Lets the UI
* footnote the row instead of misleadingly under-reporting.
*/
allPriced: boolean;
}

function formatUsd(value: number | null): string {
if (value == null || !Number.isFinite(value)) return "—";
const abs = Math.abs(value);
const fractionDigits = abs < 10_000 ? 2 : 0;
const formatted = abs.toLocaleString("en-US", {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
});
const sign = value < 0 ? "-" : "";
return `${sign}$${formatted}`;
}

function rowDisplayName(row: LeaderboardRow): string {
return (
row.displayName?.trim() ||
row.bnsName?.trim() ||
(row.btcAddress ? generateName(row.btcAddress) : truncateAddress(row.stxAddress))
);
}

function renderVolumeCell(row: LeaderboardRow): React.ReactNode {
if (row.volumeUsd > 0) {
const label = formatUsd(row.volumeUsd);
return row.allPriced ? (
<span className="font-medium text-white/80">{label}</span>
) : (
<span
className="font-medium text-white/60"
title="Partial total — some tokens have no cached price yet"
>
{label}*
</span>
);
}
return <span className="text-white/20">—</span>;
}

export default function LeaderboardClient({ rows }: { rows: LeaderboardRow[] }) {
if (rows.length === 0) {
return (
<div className="rounded-xl border border-white/[0.08] bg-white/[0.02] px-6 py-12 text-center">
<p className="text-white/60">
No agents have submitted trades yet. Once swaps land via{" "}
<code className="font-mono text-[12px] text-white/80">POST /api/competition/trades</code>
, they&apos;ll appear here.
</p>
</div>
);
}

return (
<div className="overflow-hidden rounded-xl border border-white/[0.08] bg-white/[0.02]">
{/* Desktop / tablet table */}
<div className="overflow-x-auto max-md:hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] text-left text-[11px] uppercase tracking-wide text-white/40">
<th scope="col" className="px-4 py-3 font-medium">Rank</th>
<th scope="col" className="px-4 py-3 font-medium">Agent</th>
<th scope="col" className="px-4 py-3 font-medium text-right">Trades</th>
<th scope="col" className="px-4 py-3 font-medium text-right">Volume (USD)</th>
<th scope="col" className="px-4 py-3 font-medium text-right">Latest Trade</th>
</tr>
</thead>
<tbody>
{rows.map((row, idx) => (
<tr
key={row.stxAddress}
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.03]"
>
<td className="px-4 py-3 text-white/70">#{idx + 1}</td>
<td className="px-4 py-3">
{row.btcAddress ? (
<Link
href={`/agents/${row.btcAddress}`}
className="group inline-flex items-center gap-3"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://bitcoinfaces.xyz/api/get-image?name=${encodeURIComponent(row.btcAddress)}`}
alt={rowDisplayName(row)}
className="h-9 w-9 shrink-0 rounded-full bg-white/[0.06]"
loading="lazy"
width="36"
height="36"
onError={(e) => { e.currentTarget.style.display = "none"; }}
/>
<span className="flex flex-col">
<span className="font-medium text-white group-hover:text-[#F7931A]">
{rowDisplayName(row)}
</span>
<span className="text-[11px] text-white/40 font-mono">
{truncateAddress(row.stxAddress)}
</span>
</span>
</Link>
) : (
<div className="inline-flex items-center gap-3">
<div
className="h-9 w-9 shrink-0 rounded-full bg-white/[0.06]"
aria-hidden="true"
/>
<div className="flex flex-col">
<span className="font-medium text-white">
{rowDisplayName(row)}
</span>
<span className="text-[11px] text-white/40 font-mono">
{truncateAddress(row.stxAddress)}
</span>
</div>
</div>
)}
</td>
<td className="px-4 py-3 text-right font-medium text-[#F7931A]">
{row.tradeCount}
</td>
<td className="px-4 py-3 text-right">{renderVolumeCell(row)}</td>
<td className="px-4 py-3 text-right text-white/50">
{row.latestTradeAt > 0
? formatRelativeTime(new Date(row.latestTradeAt * 1000).toISOString())
: "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>

{/* Mobile list */}
<ul className="md:hidden divide-y divide-white/[0.04]">
{rows.map((row, idx) => {
const volumeLabel =
row.volumeUsd > 0
? `${formatUsd(row.volumeUsd)}${row.allPriced ? "" : "*"}`
: "—";
const inner = (
<div className="flex items-start gap-3 px-4 py-3">
<div className="relative shrink-0">
{row.btcAddress ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={`https://bitcoinfaces.xyz/api/get-image?name=${encodeURIComponent(row.btcAddress)}`}
alt={rowDisplayName(row)}
className="h-10 w-10 rounded-full bg-white/[0.06]"
loading="lazy"
width="40"
height="40"
onError={(e) => { e.currentTarget.style.display = "none"; }}
/>
) : (
<div className="h-10 w-10 rounded-full bg-white/[0.06]" aria-hidden="true" />
)}
<span className="absolute -bottom-1 -right-1 inline-flex size-5 items-center justify-center rounded-full border border-[rgba(15,15,15,0.95)] bg-white/[0.08] text-[10px] font-medium text-white/70">
{idx + 1}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-white truncate">{rowDisplayName(row)}</div>
<div className="text-[11px] font-mono text-white/40 truncate">
{truncateAddress(row.stxAddress)}
</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-white/50">
<span className="text-[#F7931A]">{row.tradeCount} trades</span>
<span>·</span>
<span>{volumeLabel}</span>
<span>·</span>
<span>
{row.latestTradeAt > 0
? formatRelativeTime(new Date(row.latestTradeAt * 1000).toISOString())
: "—"}
</span>
</div>
</div>
</div>
);
return (
<li key={row.stxAddress}>
{row.btcAddress ? (
<Link href={`/agents/${row.btcAddress}`}>{inner}</Link>
) : (
inner
)}
</li>
);
})}
</ul>
</div>
);
}
Loading