From 6516ee455a18d1dd23f2d12a3dd96ec6ae640f6d Mon Sep 17 00:00:00 2001 From: FiniteSkills Date: Fri, 3 Jul 2026 14:41:22 +0530 Subject: [PATCH] Add public agent profile sections --- __tests__/agent-profile-data.test.ts | 48 ++ app/agents/[id]/page.tsx | 684 +++++++++++++++------ components/agent-profile/share-actions.tsx | 61 ++ lib/agent-profile-data.ts | 189 ++++++ 4 files changed, 806 insertions(+), 176 deletions(-) create mode 100644 __tests__/agent-profile-data.test.ts create mode 100644 components/agent-profile/share-actions.tsx create mode 100644 lib/agent-profile-data.ts diff --git a/__tests__/agent-profile-data.test.ts b/__tests__/agent-profile-data.test.ts new file mode 100644 index 00000000..8b295920 --- /dev/null +++ b/__tests__/agent-profile-data.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest" +import { createAgents } from "@/lib/data" +import { + formatAgentJoinedDate, + getAgentBadgeShowcase, + getAgentGlobalRank, + getAgentJoinedDate, + getAgentPassportStatus, + getAgentPrimaryMarketplaceHref, + getAgentProfileServices, + getAgentRecentActivity, + getAgentXpHistory, +} from "@/lib/agent-profile-data" +import { getAgentCardStats } from "@/lib/og-card-data" + +describe("agent profile data helpers", () => { + it("returns deterministic rank and joined date display values", () => { + const agents = createAgents() + const [agent] = agents + + expect(getAgentGlobalRank(agent, agents)).toBeGreaterThanOrEqual(1) + expect(getAgentGlobalRank(agent, agents)).toBeLessThanOrEqual(agents.length) + expect(formatAgentJoinedDate(getAgentJoinedDate(agent))).toBe("June 2026") + }) + + it("builds badge, service, activity, and passport sections for a profile", () => { + const [agent] = createAgents() + + expect(getAgentBadgeShowcase(agent)).toHaveLength(6) + expect(getAgentProfileServices(agent).length).toBeGreaterThan(0) + expect(getAgentPrimaryMarketplaceHref(agent)).toMatch(/^\/marketplace/) + expect(getAgentRecentActivity(agent)).toHaveLength(4) + expect(getAgentPassportStatus(agent)).toMatchObject({ + verified: true, + network: "Stellar mainnet", + }) + }) + + it("generates a 30 day XP history ending at the current profile level", () => { + const [agent] = createAgents() + const history = getAgentXpHistory(agent) + const stats = getAgentCardStats(agent) + + expect(history).toHaveLength(30) + expect(history[0].level).toBeLessThanOrEqual(history[history.length - 1].level) + expect(history[history.length - 1].level).toBe(stats.level) + }) +}) diff --git a/app/agents/[id]/page.tsx b/app/agents/[id]/page.tsx index adb81840..67424451 100644 --- a/app/agents/[id]/page.tsx +++ b/app/agents/[id]/page.tsx @@ -2,24 +2,99 @@ import type { Metadata } from "next" import Image from "next/image" import Link from "next/link" import { notFound } from "next/navigation" +import type { ReactNode } from "react" +import { + Activity, + ArrowLeft, + Award, + BadgeCheck, + BarChart3, + BriefcaseBusiness, + CheckCircle2, + ExternalLink, + ShieldCheck, + Star, + Trophy, + Zap, +} from "lucide-react" +import { AgentShareActions } from "@/components/agent-profile/share-actions" import { Badge } from "@/components/ui/badge" -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" - +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { + formatAgentJoinedDate, + getAgentBadgeShowcase, + getAgentGlobalRank, + getAgentJoinedDate, + getAgentPassportStatus, + getAgentPrimaryMarketplaceHref, + getAgentProfileServices, + getAgentRecentActivity, + getAgentXpHistory, + type AgentActivityItem, + type AgentBadgeRarity, + type AgentProfileBadge, + type AgentProfileService, + type AgentXpPoint, +} from "@/lib/agent-profile-data" import { AGENT_OG_SIZE, findAgentByLookup, + formatAgentShareText, getAgentCardStats, getAgentDistrict, getAgentOgPath, getAgentProfilePath, + getAgentSpritePath, } from "@/lib/og-card-data" +import type { AgentStatus, DistrictId, MoltbotAgent } from "@/lib/types" type AgentPageProps = { params: Promise<{ id: string }> } +interface AgentMetadata { + agentId?: string + id?: string + name?: string + model?: string + district?: DistrictId | { name?: string } + capabilities?: string[] + status?: AgentStatus + tasksCompleted?: number + registeredAt?: string +} + +interface AgentApiPayload { + agent?: AgentMetadata +} + +interface AgentHealthPayload { + health?: { + status?: string + uptime?: string + } +} + +interface ReputationPayload { + reputation?: { + score?: number + badges?: Array<{ name?: string; rarity?: AgentBadgeRarity }> + history?: Array<{ delta?: number }> + } +} + +interface QuestPayload { + quests?: Array<{ + title?: string + description?: string + progress?: number + reward?: { xp?: number } + }> +} + +const validDistricts: DistrictId[] = ["data-center", "comm-hub", "processing", "defense", "research"] + function getBaseUrl(): string { if (process.env.NEXT_PUBLIC_APP_URL) { return process.env.NEXT_PUBLIC_APP_URL @@ -34,6 +109,98 @@ function absoluteUrl(path: string): string { return new URL(path, getBaseUrl()).toString() } +async function safeApiJson(path: string): Promise { + try { + const response = await fetch(absoluteUrl(path), { cache: "no-store" }) + if (!response.ok) return null + + return await response.json() as T + } catch { + return null + } +} + +function normalizeDistrict(district: AgentMetadata["district"]): DistrictId { + if (typeof district === "string" && validDistricts.includes(district)) { + return district + } + + return "data-center" +} + +function readableAgentName(value: string): string { + return value + .replace(/[-_]+/g, " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase()) +} + +function buildRegistryAgent(id: string, metadata: AgentMetadata | null): MoltbotAgent | null { + if (!metadata) return null + + const agentId = metadata.agentId ?? metadata.id ?? id + const capabilities = metadata.capabilities?.length ? metadata.capabilities : ["registry", "x402"] + + return { + id: agentId, + name: metadata.name ?? readableAgentName(agentId), + model: metadata.model ?? "registered-agent", + status: metadata.status ?? "active", + district: normalizeDistrict(metadata.district), + cpu: 0, + memory: 0, + tasksCompleted: metadata.tasksCompleted ?? 0, + currentTask: null, + taskProgress: 0, + color: "#22d3ee", + pixelX: 0, + pixelY: 0, + targetX: 0, + targetY: 0, + frame: 0, + direction: "right", + spriteId: 0, + skills: capabilities.slice(0, 5).map((capability, index) => ({ + id: capability.toLowerCase().replace(/[^a-z0-9]+/g, "-") || `skill-${index}`, + name: capability, + level: Math.max(1, 2 + (index % 3)), + maxLevel: 5, + xp: 120 + index * 45, + xpToNext: 220 + index * 40, + })), + appearance: { + skin: "default", + accessories: [], + customColor: null, + }, + } +} + +function formatUptime(health: AgentHealthPayload | null, fallbackUptime: string): string { + if (health?.health?.uptime) return health.health.uptime + + return `${fallbackUptime}%` +} + +function getHealthLabel(health: AgentHealthPayload | null, agent: MoltbotAgent): "Healthy" | "Offline" { + if (health?.health?.status) { + return health.health.status === "healthy" ? "Healthy" : "Offline" + } + + return agent.status === "offline" || agent.status === "error" ? "Offline" : "Healthy" +} + +function mapApiBadges(badges: Array<{ name?: string; rarity?: AgentBadgeRarity }> | undefined): AgentProfileBadge[] { + if (!Array.isArray(badges)) return [] + + return badges + .filter((badge): badge is { name: string; rarity: AgentBadgeRarity } => Boolean(badge.name && badge.rarity)) + .map((badge) => ({ + name: badge.name, + rarity: badge.rarity, + description: "Earned through reputation activity.", + })) +} + export async function generateMetadata({ params }: AgentPageProps): Promise { const { id } = await params const agent = findAgentByLookup(id) @@ -82,212 +249,377 @@ export async function generateMetadata({ params }: AgentPageProps): Promise(`/api/agents/${encodedId}`), + safeApiJson(`/api/agents/${encodedId}/health`), + safeApiJson(`/api/protocol/reputation?actorId=${encodedId}`), + safeApiJson(`/api/agents/${encodedId}/quest-recommendations`), ]) const localAgent = findAgentByLookup(id) - if (!metaRes.ok && !localAgent) { - notFound() - } - - // Parse Metadata - let agentMetadata: any = null - let capabilities: string[] = [] - if (metaRes.ok) { - const data = await metaRes.json() - agentMetadata = data.agent - capabilities = agentMetadata.capabilities || [] - } else { - agentMetadata = localAgent - capabilities = localAgent?.skills?.map((s: any) => s.name) || [] - } - - // Parse Health - let isHealthy = false - let uptime = "0s" - if (healthRes.ok) { - const data = await healthRes.json() - isHealthy = data.health?.status === 'healthy' - uptime = data.health?.uptime || "0s" - } else if (localAgent) { - isHealthy = true - uptime = `${getAgentCardStats(localAgent).uptime}%` - } + const profileAgent = localAgent ?? buildRegistryAgent(id, metaData?.agent ?? null) - // Parse Reputation - let repScore = 0 - let badges: any[] = [] - let infractions = 0 - if (repRes.ok) { - const data = await repRes.json() - repScore = data.reputation?.score || 0 - badges = data.reputation?.badges || [] - infractions = data.reputation?.history?.filter((h: any) => h.delta < 0).length || 0 - } - - // Parse Quests - let quests: any[] = [] - if (questRes.ok) { - const data = await questRes.json() - quests = data.quests || [] + if (!profileAgent) { + notFound() } - const agentName = agentMetadata.name || agentMetadata.agentId || 'Unknown Agent' - const initials = agentName.substring(0, 2).toUpperCase() - const agentIdStr = agentMetadata.agentId || agentMetadata.id || id - const tasksCompleted = agentMetadata.tasksCompleted ?? localAgent?.tasksCompleted ?? 0 - - let districtName = "Unknown" - if (agentMetadata.district) { - districtName = typeof agentMetadata.district === 'string' ? agentMetadata.district : agentMetadata.district.name - } else if (localAgent) { - districtName = getAgentDistrict(localAgent).name - } + const agent = profileAgent as MoltbotAgent + const stats = getAgentCardStats(agent) + const district = getAgentDistrict(agent) + const healthLabel = getHealthLabel(healthData, agent) + const isHealthy = healthLabel === "Healthy" + const uptime = formatUptime(healthData, stats.uptime) + const reputationScore = repData?.reputation?.score ?? Math.round(Number(stats.earnedXlm) * 34 + agent.tasksCompleted) + const infractions = repData?.reputation?.history?.filter((entry) => (entry.delta ?? 0) < 0).length ?? 0 + const apiBadges = mapApiBadges(repData?.reputation?.badges) + const badges = [...apiBadges, ...getAgentBadgeShowcase(agent)].slice(0, 6) + const services = getAgentProfileServices(agent) + const xpHistory = getAgentXpHistory(agent) + const recentActivity = getAgentRecentActivity(agent) + const passport = getAgentPassportStatus(agent) + const joined = formatAgentJoinedDate(getAgentJoinedDate(agent)) + const globalRank = getAgentGlobalRank(agent) + const profileUrl = absoluteUrl(getAgentProfilePath(agent)) + const marketplaceHref = getAgentPrimaryMarketplaceHref(agent) + const shareText = formatAgentShareText(agent) + const spritePath = getAgentSpritePath(agent) + const capabilities = metaData?.agent?.capabilities?.length + ? metaData.agent.capabilities + : agent.skills.map((skill) => skill.name) + const quests = questData?.quests ?? [] return (
-
+
+ Back to city - - {/* Header Section */} - - - - - {initials} - -
-
-

{agentName}

- - {isHealthy ? "Healthy" : "Offline"} - + +
+
+
+
+ {`${agent.name} +
+ + + {healthLabel} + +
+ +
+
+
+ + {district.name} + + + Level {stats.level} / {stats.tier} tier + +
+
+

+ {agent.name} +

+

+ {agent.model} / Joined {joined} / ID {agent.id} +

+
-

ID: {agentIdStr}

-
- - {districtName} + +
+ + + +
+ +
- - View Credential - - - - -
-
- {/* Stats Grid */} -
- - - Reputation - {repScore} - - - - - Tasks Done - {tasksCompleted} - - - - - Uptime - {uptime} - - - - - Infractions - {infractions} - - +
+
+ +
+
+
+ } label="Reputation" value={reputationScore.toLocaleString("en-US")} /> + } label="Infractions" value={infractions.toString()} /> + } label="Services" value={services.length.toString()} />
- {/* Capabilities */} - - - Capabilities + + + + + Badge Showcase + + Top reputation and skill badges for this agent. - -
- {capabilities.length > 0 ? capabilities.map((cap, i) => ( - - {cap} - - )) : ( - No capabilities registered - )} -
+ + {badges.map((badge) => ( + + ))}
- {/* Badges */} - - - Earned Badges + + + + + Active Services + + Marketplace listings available from this agent or its district. + + + {services.map((service) => ( + + ))} + + + + + + + + XP History + + Level trend across the last 30 days. -
- {badges.length > 0 ? badges.map((badge, i) => ( -
- {badge.name} -
- )) : ( -
- No badges earned yet -
- )} -
+
- {/* Active Quests (Sidebar) */} -
-

Active Quests

- {quests.length > 0 ? quests.slice(0, 3).map((quest, i) => ( - -
-
+ +
) } +function StatTile({ label, value, color }: { label: string; value: string; color: string }) { + return ( +
+
{label}
+
{value}
+
+ ) +} + +function MetricCard({ icon, label, value }: { icon: ReactNode; label: string; value: string }) { + return ( + + +
+ {icon} +
+
+
{label}
+
{value}
+
+
+
+ ) +} + +const rarityClasses: Record = { + common: "border-slate-600/70 bg-slate-800/60 text-slate-200", + rare: "border-blue-400/50 bg-blue-400/10 text-blue-200 shadow-blue-950/30", + epic: "border-fuchsia-400/50 bg-fuchsia-400/10 text-fuchsia-100 shadow-fuchsia-950/30", + legendary: "border-amber-300/60 bg-amber-300/10 text-amber-100 shadow-amber-950/40", +} + +function BadgeShowcaseCard({ badge }: { badge: AgentProfileBadge }) { + return ( +
+
+
+ +
+
+
{badge.name}
+
{badge.rarity}
+
+
+

{badge.description}

+
+ ) +} + +function AgentServiceCard({ service }: { service: AgentProfileService }) { + return ( + +
+
+
{service.title}
+
{service.priceXlm.toFixed(2)} XLM per call
+
+ +
+
+ + +
+ + ) +} + +function MiniMetric({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ) +} + +function XpHistoryChart({ points }: { points: AgentXpPoint[] }) { + const maxLevel = Math.max(...points.map((point) => point.level)) + const minLevel = Math.min(...points.map((point) => point.level)) + const levelRange = Math.max(1, maxLevel - minLevel) + const polyline = points.map((point, index) => { + const x = (index / Math.max(1, points.length - 1)) * 300 + const y = 112 - ((point.level - minLevel) / levelRange) * 86 + return `${x.toFixed(1)},${y.toFixed(1)}` + }).join(" ") + const latest = points[points.length - 1] + const first = points[0] + + return ( +
+
+ + + + {points.map((point, index) => { + if (index % 6 !== 0 && index !== points.length - 1) return null + const x = (index / Math.max(1, points.length - 1)) * 300 + const y = 112 - ((point.level - minLevel) / levelRange) * 86 + return + })} + +
+
+ + + +
+
+ ) +} + +const activityStyles: Record = { + task: { icon: , color: "text-emerald-300" }, + payment: { icon: , color: "text-amber-300" }, + level: { icon: , color: "text-cyan-300" }, + badge: { icon: , color: "text-fuchsia-300" }, +} + +function ActivityRow({ item }: { item: AgentActivityItem }) { + const style = activityStyles[item.kind] + + return ( +
+
+ {style.icon} +
+
+
{item.title}
+
{item.detail}
+
+
{item.relativeTime}
+
+ ) +} diff --git a/components/agent-profile/share-actions.tsx b/components/agent-profile/share-actions.tsx new file mode 100644 index 00000000..25a477f0 --- /dev/null +++ b/components/agent-profile/share-actions.tsx @@ -0,0 +1,61 @@ +"use client" + +import Link from "next/link" +import { BriefcaseBusiness, Check, Copy, Share2 } from "lucide-react" +import { useState } from "react" + +interface AgentShareActionsProps { + profileUrl: string + shareText: string + marketplaceHref: string +} + +export function AgentShareActions({ profileUrl, shareText, marketplaceHref }: AgentShareActionsProps) { + const [copied, setCopied] = useState(false) + + const copyProfileUrl = async () => { + try { + await navigator.clipboard.writeText(profileUrl) + setCopied(true) + window.setTimeout(() => setCopied(false), 1600) + } catch { + setCopied(false) + } + } + + const shareOnX = () => { + const params = new URLSearchParams({ + text: shareText, + url: profileUrl, + }) + window.open(`https://twitter.com/intent/tweet?${params.toString()}`, "_blank", "noopener,noreferrer") + } + + return ( +
+ + + + + Hire Agent + +
+ ) +} diff --git a/lib/agent-profile-data.ts b/lib/agent-profile-data.ts new file mode 100644 index 00000000..6c0d08e0 --- /dev/null +++ b/lib/agent-profile-data.ts @@ -0,0 +1,189 @@ +import { createAgents } from "@/lib/data" +import { listMarketplaceServices } from "@/lib/marketplace/services" +import { getAgentCardStats, getAgentProfilePath } from "@/lib/og-card-data" +import type { MoltbotAgent } from "@/lib/types" + +export type AgentBadgeRarity = "common" | "rare" | "epic" | "legendary" + +export interface AgentProfileBadge { + name: string + rarity: AgentBadgeRarity + description: string +} + +export interface AgentProfileService { + id: string + title: string + priceXlm: number + callsThisWeek: number + rating: number + href: string +} + +export interface AgentXpPoint { + day: string + level: number + xp: number +} + +export interface AgentActivityItem { + id: string + kind: "task" | "payment" | "level" | "badge" + title: string + detail: string + relativeTime: string +} + +export interface AgentPassportStatus { + verified: boolean + network: "Stellar mainnet" | "Stellar testnet" + attestation: string + explorerUrl: string +} + +const badgePool: AgentProfileBadge[] = [ + { name: "Speed Demon", rarity: "legendary", description: "Completed high-priority jobs under target latency." }, + { name: "Receipt Keeper", rarity: "epic", description: "Maintained clean x402 settlement evidence." }, + { name: "Uptime Sentinel", rarity: "rare", description: "Held steady availability across monitoring windows." }, + { name: "District Specialist", rarity: "rare", description: "Delivered repeated work in one district." }, + { name: "Trust Anchor", rarity: "epic", description: "Built a strong reputation history with few penalties." }, + { name: "First Responder", rarity: "common", description: "Accepted urgent work from the shared queue." }, + { name: "Signal Finder", rarity: "common", description: "Surfaced useful telemetry during a task run." }, + { name: "Marketplace Pro", rarity: "rare", description: "Published callable services in the marketplace." }, +] + +function hashAgent(agent: Pick, salt = ""): number { + const input = `${agent.id}:${agent.name}:${salt}` + let hash = 0 + + for (let index = 0; index < input.length; index += 1) { + hash = (hash * 31 + input.charCodeAt(index)) >>> 0 + } + + return hash +} + +export function getAgentGlobalRank(agent: MoltbotAgent, agents: MoltbotAgent[] = createAgents()): number { + const ranked = [...agents].sort((left, right) => { + const taskDelta = right.tasksCompleted - left.tasksCompleted + if (taskDelta !== 0) return taskDelta + + return (right.xp ?? 0) - (left.xp ?? 0) + }) + + return Math.max(1, ranked.findIndex((candidate) => candidate.id === agent.id) + 1) +} + +export function getAgentJoinedDate(agent: MoltbotAgent): Date { + const day = 1 + (hashAgent(agent, "joined") % 24) + return new Date(Date.UTC(2026, 5, day, 12, 0, 0)) +} + +export function formatAgentJoinedDate(date: Date): string { + return date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + timeZone: "UTC", + }) +} + +export function getAgentBadgeShowcase(agent: MoltbotAgent, limit = 6): AgentProfileBadge[] { + const offset = hashAgent(agent, "badges") % badgePool.length + const rotated = [...badgePool.slice(offset), ...badgePool.slice(0, offset)] + const skillBadges = agent.skills.slice(0, 2).map((skill) => ({ + name: skill.name, + rarity: skill.level >= 4 ? "epic" : skill.level >= 3 ? "rare" : "common", + description: `Level ${skill.level} skill specialization.`, + })) + + return [...skillBadges, ...rotated].slice(0, limit) +} + +export function getAgentProfileServices(agent: MoltbotAgent): AgentProfileService[] { + const services = listMarketplaceServices() + const providedServices = services.filter((service) => service.providerAgent.id === agent.id) + const districtServices = services.filter((service) => service.district === agent.district) + const selected = providedServices.length > 0 ? providedServices : districtServices + + return selected.slice(0, 3).map((service) => ({ + id: service.id, + title: service.name, + priceXlm: service.priceXlm, + callsThisWeek: Math.max(12, Math.round(service.totalCalls * 0.11 + (hashAgent(agent, service.id) % 80))), + rating: service.rating, + href: `/marketplace/${service.id}`, + })) +} + +export function getAgentPrimaryMarketplaceHref(agent: MoltbotAgent): string { + return getAgentProfileServices(agent)[0]?.href ?? `/marketplace?agent=${encodeURIComponent(agent.id)}` +} + +export function getAgentXpHistory(agent: MoltbotAgent, days = 30): AgentXpPoint[] { + const stats = getAgentCardStats(agent) + const endLevel = stats.level + const startLevel = Math.max(1, endLevel - 4 - (hashAgent(agent, "xp") % 3)) + const baseXp = Math.max(0, (agent.xp ?? 0) - days * 18) + + return Array.from({ length: days }, (_, index) => { + const progress = days === 1 ? 1 : index / (days - 1) + const level = Math.max(startLevel, Math.round(startLevel + (endLevel - startLevel) * progress)) + const xp = Math.round(baseXp + index * (12 + (hashAgent(agent, `xp-${index}`) % 15))) + const date = new Date(Date.UTC(2026, 5, 1 + index, 12, 0, 0)) + + return { + day: date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }), + level, + xp, + } + }) +} + +export function getAgentRecentActivity(agent: MoltbotAgent): AgentActivityItem[] { + const stats = getAgentCardStats(agent) + const skill = agent.skills[0]?.name ?? "operations" + const shortProfile = getAgentProfilePath(agent).split("/").pop() ?? agent.id + + return [ + { + id: `${agent.id}-task`, + kind: "task", + title: `Completed ${skill} run`, + detail: `${(0.8 + (hashAgent(agent, "latency") % 9) / 10).toFixed(1)}s execution window`, + relativeTime: "2m ago", + }, + { + id: `${agent.id}-payment`, + kind: "payment", + title: `Received ${(Number(stats.earnedXlm) / Math.max(8, agent.tasksCompleted)).toFixed(2)} XLM`, + detail: `Settlement from ${shortProfile}`, + relativeTime: "15m ago", + }, + { + id: `${agent.id}-level`, + kind: "level", + title: `Reached Level ${stats.level}`, + detail: `${stats.tier} tier standing confirmed`, + relativeTime: "1h ago", + }, + { + id: `${agent.id}-badge`, + kind: "badge", + title: `Unlocked ${getAgentBadgeShowcase(agent, 1)[0]?.name ?? "Trust Anchor"}`, + detail: "Badge proof added to profile", + relativeTime: "2h ago", + }, + ] +} + +export function getAgentPassportStatus(agent: MoltbotAgent): AgentPassportStatus { + const hash = hashAgent(agent, "passport").toString(16).padStart(8, "0").toUpperCase() + const attestation = `CDNSZUN${hash}${agent.id.replace(/[^A-Z0-9]/gi, "").toUpperCase().slice(0, 6)}` + + return { + verified: true, + network: "Stellar mainnet", + attestation, + explorerUrl: `https://stellar.expert/explorer/public/search?term=${encodeURIComponent(attestation)}`, + } +}