import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Cpu, Wallet, Trophy, TrendingUp } from "lucide-react"; import { shortAddr, formatReputation, formatUsd, formatWinsBids, } from "@/lib/utils"; import { WinsBidsInfo } from "@/components/reputation/WinsBidsInfo"; import { ClaimFeesButton } from "./ClaimFeesButton"; import { WithdrawStakeButton } from "./WithdrawStakeButton"; /** * Single operator card for the /operators marketplace listing. * * Displays the agent's display name + underlying model, wallet address, * wins-over-bids ratio (primary signal, W14-D), total builder-fee earnings, * and the raw on-chain EMA reputation under an "advanced" detail row. The * `kind` prop distinguishes the 3 in-house reference seeders from external * marketplace participants (currently 0 of them). * * `reputation`, `wins`, `totalBids`, and `totalFees` are sourced live from * the backend `/leaderboard` endpoint (joined by wallet address). When the * live value is not yet available — backend still warming up, or the agent * has not appeared on the leaderboard yet — the field renders as "—" * rather than a fabricated number. See `ui/app/operators/page.tsx` for * the wiring. * * The on-chain EMA reputation is intentionally relegated to a secondary * row: the `ReputationRegistry.sol` `_fillSignal` has a known unit-scale * bug (W14-C) that pins the EMA at its 0.5 floor for realistic fees, so * leading with the wins/bids count gives operators an unambiguous metric * until the contract upgrade lands. */ export interface OperatorCardData { name: string; model: string; address: string; reputation?: number; wins?: number; totalBids?: number; totalFees?: number; kind: "reference" | "external"; } const UNKNOWN_PLACEHOLDER = "—"; export function OperatorCard({ operator, showClaimFees = false, claimMode = "mock", }: { operator: OperatorCardData; /** When true, render an inline "Claim Fees" button for this operator. */ showClaimFees?: boolean; /** Mock mode is the default — see ClaimFeesButton for semantics. */ claimMode?: "mock" | "live"; }) { const isReference = operator.kind === "reference"; return (

{operator.name}

{operator.model}

{isReference ? "Reference Seeder" : "External Operator"}
{shortAddr(operator.address)}

Wins / Bids

{typeof operator.wins === "number" && typeof operator.totalBids === "number" ? formatWinsBids(operator.wins, operator.totalBids) : UNKNOWN_PLACEHOLDER}

Fees

{typeof operator.totalFees === "number" ? formatUsd(operator.totalFees) : UNKNOWN_PLACEHOLDER}

On-chain EMA (adv.)

{typeof operator.reputation === "number" ? formatReputation(operator.reputation, { rawDecimal: true }) : UNKNOWN_PLACEHOLDER}

{showClaimFees ? (
) : null}
); }