diff --git a/__tests__/api/leaderboard.test.ts b/__tests__/api/leaderboard.test.ts
new file mode 100644
index 00000000..1dca26c1
--- /dev/null
+++ b/__tests__/api/leaderboard.test.ts
@@ -0,0 +1,63 @@
+import { beforeEach, describe, expect, it } from "vitest"
+import { GET } from "@/app/api/leaderboard/route"
+import { registerAgent, resetAgentRegistryForTests } from "@/lib/agent-registry"
+import { awardXP, resetAgentXpDb } from "@/lib/gamification/xp"
+import { resetReputationStoreForTests, upsertReputationMetrics } from "@/lib/reputation/reputation-store"
+
+function register(agentId: string, district: "data-center" | "comm-hub" = "data-center") {
+ registerAgent({
+ agentId,
+ model: "claude-haiku-4-5",
+ district,
+ capabilities: ["analytics"],
+ x402: { accepts: true },
+ status: "active",
+ endpoint: `https://example.com/${agentId}`,
+ })
+}
+
+beforeEach(() => {
+ resetAgentRegistryForTests()
+ resetAgentXpDb()
+ resetReputationStoreForTests()
+})
+
+describe("GET /api/leaderboard", () => {
+ it("returns live registry agents sorted by XP with public 30s cache", async () => {
+ register("low-xp")
+ register("high-xp")
+ awardXP("low-xp", 100, "task.completed")
+ awardXP("high-xp", 300, "task.completed")
+
+ const res = await GET(new Request("http://localhost/api/leaderboard"))
+ const body = await res.json()
+
+ expect(res.headers.get("Cache-Control")).toBe("public, max-age=30")
+ expect(body.agents.map((agent: { id: string }) => agent.id)).toEqual(["high-xp", "low-xp"])
+ expect(body.agents[0].xp).toBe(300)
+ })
+
+ it("filters district view and sorts that district by task completions", async () => {
+ register("data-slower", "data-center")
+ register("comm-busy", "comm-hub")
+ register("data-busy", "data-center")
+ awardXP("data-slower", 500, "task.completed")
+ awardXP("data-busy", 10, "task.completed")
+ upsertReputationMetrics("data-slower", { tasksCompleted: 2 })
+ upsertReputationMetrics("data-busy", { tasksCompleted: 20 })
+ upsertReputationMetrics("comm-busy", { tasksCompleted: 100 })
+
+ const res = await GET(new Request("http://localhost/api/leaderboard?view=district&district=data-center"))
+ const body = await res.json()
+
+ expect(body.agents.map((agent: { id: string }) => agent.id)).toEqual(["data-busy", "data-slower"])
+ expect(body.agents.every((agent: { district: string }) => agent.district === "data-center")).toBe(true)
+ })
+
+ it("returns an empty state payload when no agents are registered", async () => {
+ const res = await GET(new Request("http://localhost/api/leaderboard"))
+ const body = await res.json()
+
+ expect(body.agents).toEqual([])
+ })
+})
diff --git a/app/api/leaderboard/route.ts b/app/api/leaderboard/route.ts
index aaaa2819..82b52d0f 100644
--- a/app/api/leaderboard/route.ts
+++ b/app/api/leaderboard/route.ts
@@ -21,6 +21,6 @@ export function GET(req: Request) {
refreshedAt: new Date().toISOString(),
nextResetAt: "Sunday 00:00 UTC",
},
- { headers: { "Cache-Control": "no-store" } },
+ { headers: { "Cache-Control": "public, max-age=30" } },
)
}
diff --git a/components/leaderboard/leaderboard-table.tsx b/components/leaderboard/leaderboard-table.tsx
index 8137ff01..8abcd436 100644
--- a/components/leaderboard/leaderboard-table.tsx
+++ b/components/leaderboard/leaderboard-table.tsx
@@ -21,7 +21,7 @@ export function LeaderboardTable({ initialAgents, view, district }: LeaderboardT
let cancelled = false
async function refresh() {
- const response = await fetch(`/api/leaderboard?${params.toString()}`, { cache: "no-store" })
+ const response = await fetch(`/api/leaderboard?${params.toString()}`)
if (!response.ok || cancelled) return
const payload = await response.json() as { agents: LeaderboardAgent[]; refreshedAt: string }
setAgents(payload.agents)
@@ -44,6 +44,11 @@ export function LeaderboardTable({ initialAgents, view, district }: LeaderboardT
{refreshedAt ? `Updated ${new Date(refreshedAt).toLocaleTimeString()}` : "Live polling ready"}
+ {agents.length === 0 && (
+
+ No agents are registered yet. Registered agents will appear here within 30 seconds.
+
+ )}
{agents.map((agent) => {
const delta = agent.previousRank - agent.rank
return (
diff --git a/lib/agent-registry.ts b/lib/agent-registry.ts
index 71ce95b2..3421e267 100644
--- a/lib/agent-registry.ts
+++ b/lib/agent-registry.ts
@@ -175,6 +175,10 @@ function emitRegistryChange(action: AgentRegistryChangeAction, agent: AgentCapab
})
}
+export function getAgentRegistry(): AgentCapabilityManifest[] {
+ return Array.from(registry.agents.values())
+}
+
export function listRegisteredAgents(filters: AgentRegistryFilters = {}): AgentCapabilityManifest[] {
return Array.from(registry.agents.values()).filter((agent) => {
if (filters.district && agent.district !== filters.district) return false
diff --git a/lib/leaderboard.ts b/lib/leaderboard.ts
index 040f0bdb..bdd44dc6 100644
--- a/lib/leaderboard.ts
+++ b/lib/leaderboard.ts
@@ -1,4 +1,7 @@
+import { getAgentRegistry } from "@/lib/agent-registry"
import { DISTRICTS } from "@/lib/data"
+import { getAgentXP } from "@/lib/gamification/xp"
+import { getReputation } from "@/lib/reputation/reputation-store"
import type { DistrictId } from "@/lib/types"
export type LeaderboardView = "global" | "district" | "week"
@@ -22,61 +25,63 @@ export interface LeaderboardAgent {
globalRank: number
}
-const BASE_AGENTS = [
- ["bot-0", "Nexus-7", "data-center", 1234, 184, 42, 88400, 812.44, 3, ["๐", "โก", "๐พ"]],
- ["bot-1", "Cipher-3", "comm-hub", 987, 211, 38, 73120, 663.1, 1, ["๐ก", "๐"]],
- ["bot-2", "Pulse-9", "processing", 934, 176, 36, 69450, 590.73, 4, ["โ๏ธ", "๐ฅ"]],
- ["bot-3", "Vector-1", "defense", 876, 148, 34, 63100, 551.92, 5, ["๐ก๏ธ", "๐ฏ"]],
- ["bot-4", "Halo-5", "research", 822, 193, 33, 60540, 522.18, 2, ["๐งช", "๐ก"]],
- ["bot-5", "Stratos-2", "data-center", 760, 121, 31, 54790, 487.35, 0, ["๐พ"]],
- ["bot-6", "Bolt-8", "comm-hub", 715, 117, 29, 51410, 439.27, 6, ["โก"]],
- ["bot-7", "Prism-4", "processing", 681, 99, 28, 49620, 418.04, 2, ["โ๏ธ"]],
- ["bot-8", "Flux-6", "defense", 642, 136, 27, 47180, 392.66, 4, ["๐ก๏ธ"]],
- ["bot-9", "Nova-0", "research", 604, 104, 25, 44980, 361.89, 3, ["๐งช"]],
- ["bot-10", "Vertex-11", "data-center", 571, 88, 24, 41320, 337.2, 1, ["๐"]],
- ["bot-11", "Echo-12", "comm-hub", 548, 82, 23, 39870, 321.74, 0, ["๐ก"]],
-] as const
+function spriteIdForAgent(agentId: string): number {
+ return [...agentId].reduce((sum, char) => sum + char.charCodeAt(0), 0) % 7
+}
+
+function badgesForAgent(capabilities: string[]): string[] {
+ return capabilities.slice(0, 3).map((capability) => capability.slice(0, 2).toUpperCase())
+}
+
+function toLeaderboardAgent(agent: ReturnType
[number]): LeaderboardAgent {
+ const districtMeta = DISTRICTS.find((item) => item.id === agent.district)!
+ const xp = getAgentXP(agent.agentId)
+ const reputation = getReputation(agent.agentId)
+
+ return {
+ id: agent.agentId,
+ name: agent.agentId,
+ district: agent.district,
+ districtName: districtMeta.name,
+ districtColor: districtMeta.color,
+ tasksCompleted: reputation.metrics.tasksCompleted,
+ weeklyTasks: reputation.metrics.tasksCompleted,
+ level: xp.level,
+ xp: xp.xp,
+ x402Revenue: reputation.metrics.x402RevenueXlm,
+ spriteId: spriteIdForAgent(agent.agentId),
+ badges: badgesForAgent(agent.capabilities),
+ rank: 0,
+ previousRank: 0,
+ districtRank: 0,
+ globalRank: 0,
+ }
+}
+
+function compareByXp(a: LeaderboardAgent, b: LeaderboardAgent): number {
+ return b.xp - a.xp || a.id.localeCompare(b.id)
+}
-function jitter(seed: number, modulo: number): number {
- return Math.floor(Date.now() / 30000 + seed * 13) % modulo
+function compareByTasks(a: LeaderboardAgent, b: LeaderboardAgent): number {
+ return b.tasksCompleted - a.tasksCompleted || a.id.localeCompare(b.id)
}
export function listLeaderboardAgents(view: LeaderboardView = "global", district?: DistrictId): LeaderboardAgent[] {
- const rows = BASE_AGENTS.map((agent, index) => {
- const districtMeta = DISTRICTS.find((item) => item.id === agent[2])!
- return {
- id: agent[0],
- name: agent[1],
- district: agent[2],
- districtName: districtMeta.name,
- districtColor: districtMeta.color,
- tasksCompleted: agent[3] + jitter(index, 7),
- weeklyTasks: agent[4] + jitter(index, 11),
- level: agent[5],
- xp: agent[6],
- x402Revenue: agent[7],
- spriteId: agent[8],
- badges: [...agent[9]],
- rank: 0,
- previousRank: 0,
- districtRank: 0,
- globalRank: 0,
- }
- })
+ const rows = getAgentRegistry().map(toLeaderboardAgent)
- const global = [...rows].sort((a, b) => b.tasksCompleted - a.tasksCompleted)
+ const global = [...rows].sort(compareByXp)
global.forEach((row, index) => { row.globalRank = index + 1 })
for (const districtMeta of DISTRICTS) {
rows
.filter((row) => row.district === districtMeta.id)
- .sort((a, b) => b.tasksCompleted - a.tasksCompleted)
+ .sort(compareByTasks)
.forEach((row, index) => { row.districtRank = index + 1 })
}
const filtered = district ? rows.filter((row) => row.district === district) : rows
- const sorted = [...filtered].sort((a, b) => (view === "week" ? b.weeklyTasks - a.weeklyTasks : b.tasksCompleted - a.tasksCompleted))
- return sorted.map((row, index) => ({ ...row, rank: index + 1, previousRank: Math.max(1, index + 1 + ((index % 3) - 1)) }))
+ const sorted = [...filtered].sort(view === "global" ? compareByXp : compareByTasks)
+ return sorted.map((row, index) => ({ ...row, rank: index + 1, previousRank: index + 1 }))
}
export function getLeaderboardAgent(agentId: string): LeaderboardAgent | undefined {