Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 63 additions & 0 deletions __tests__/api/leaderboard.test.ts
Original file line number Diff line number Diff line change
@@ -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([])
})
})
2 changes: 1 addition & 1 deletion app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" } },
)
}
7 changes: 6 additions & 1 deletion components/leaderboard/leaderboard-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -44,6 +44,11 @@ export function LeaderboardTable({ initialAgents, view, district }: LeaderboardT
<span>{refreshedAt ? `Updated ${new Date(refreshedAt).toLocaleTimeString()}` : "Live polling ready"}</span>
</div>
<div className="divide-y divide-slate-800/80">
{agents.length === 0 && (
<div className="px-4 py-12 text-center font-mono text-sm text-slate-400">
No agents are registered yet. Registered agents will appear here within 30 seconds.
</div>
)}
{agents.map((agent) => {
const delta = agent.previousRank - agent.rank
return (
Expand Down
4 changes: 4 additions & 0 deletions lib/agent-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 46 additions & 41 deletions lib/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -22,61 +25,63 @@
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

Check warning on line 29 in lib/leaderboard.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#codePointAt()` over `String#charCodeAt()`.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Open-Stellar&issues=AZ8ZMY3g7H8VEn_-94sz&open=AZ8ZMY3g7H8VEn_-94sz&pullRequest=419
}

function badgesForAgent(capabilities: string[]): string[] {
return capabilities.slice(0, 3).map((capability) => capability.slice(0, 2).toUpperCase())
}

function toLeaderboardAgent(agent: ReturnType<typeof getAgentRegistry>[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 {
Expand Down
Loading