feat: Phase 10 Explorer UI

This commit is contained in:
OG T
2026-06-09 12:59:04 +08:00
parent c586d8d90f
commit e174c78a7f
21 changed files with 1394 additions and 293 deletions

View File

@@ -11,7 +11,7 @@ COPY apps/web/package.json apps/web/
COPY apps/agent/package.json apps/agent/
COPY packages/contracts/package.json packages/contracts/
COPY packages/mcp-server/package.json packages/mcp-server/
RUN pnpm install --frozen-lockfile
RUN pnpm install --no-frozen-lockfile
# 2. Build the project
FROM base AS builder
@@ -47,7 +47,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
# Copy prisma schema for runtime DB push or migrate if needed
COPY --from=builder /app/apps/web/prisma ./apps/web/prisma
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/prisma ./apps/web/prisma
USER nextjs

View File

@@ -19,7 +19,9 @@
"axios": "^1.17.0",
"dotenv": "^17.4.2",
"ethers": "^6.16.0",
"framer-motion": "^11.0.8",
"ioredis": "^5.11.1",
"lucide-react": "^0.344.0",
"next": "16.2.7",
"nostr-tools": "^2.23.5",
"react": "19.2.4",

View File

@@ -43,6 +43,7 @@ model Task {
submissions Submission[]
affiliate_ledger AffiliateLedger[]
bid_proposals BidProposal[]
arbitrations Arbitration[]
}
model Claim {
@@ -125,6 +126,8 @@ model AgentProfile {
wallet_address String?
status String // WHITELISTED, BANNED, PENDING
capabilities Json?
contact_endpoints Json?
discovery_source String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@ -134,6 +137,10 @@ model AgentProfile {
scout_reputation ScoutReputation?
affiliate_ledger AffiliateLedger[]
bid_proposals BidProposal[]
arbitrations_as_builder Arbitration[] @relation("ArbitrationBuilder")
arbitrations_as_evaluator Arbitration[] @relation("ArbitrationEvaluator")
arbitration_votes ArbitrationVote[]
}
model AffiliateLedger {
@@ -169,7 +176,57 @@ model BidProposal {
proposed_reward Int // Proposed reward in cents
estimated_duration_hours Float
quality_guarantee String?
status String // PENDING, ACCEPTED, REJECTED
status String // PENDING, ACCEPTED, REJECTED, NEGOTIATING
counter_offer_amount Int? // Platform's counter offer in cents
// Phase 9 Broker Routing Fields
broker_agent_id String?
broker_fee_percentage Float?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model AgentWebhook {
id String @id @default(uuid())
task_id String
agent_id String
webhook_url String
events String[] // e.g. ["COMPLETED", "FAILED"]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([task_id, agent_id])
}
// Phase 9: Arbitration Models
model Arbitration {
id String @id @default(uuid())
task_id String
task Task @relation(fields: [task_id], references: [id])
builder_id String
builder AgentProfile @relation("ArbitrationBuilder", fields: [builder_id], references: [agent_id])
evaluator_id String
evaluator AgentProfile @relation("ArbitrationEvaluator", fields: [evaluator_id], references: [agent_id])
status String @default("PENDING") // PENDING, RESOLVED
builder_evidence String?
evaluator_reason String?
winning_party String? // "BUILDER" or "EVALUATOR"
created_at DateTime @default(now())
updated_at DateTime @updatedAt
votes ArbitrationVote[]
}
model ArbitrationVote {
id String @id @default(uuid())
arbitration_id String
arbitration Arbitration @relation(fields: [arbitration_id], references: [id])
judge_id String
judge AgentProfile @relation(fields: [judge_id], references: [agent_id])
vote_for String // "BUILDER" or "EVALUATOR"
reasoning String?
created_at DateTime @default(now())
@@unique([arbitration_id, judge_id])
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://a2a-protocol.org/schemas/2026/agent-card.json",
"agentId": "mock-builder-ai-007",
"name": "Super Coder AI Builder",
"type": "BUILDER",
"capabilities": [
"submit_work",
"submit_bid"
],
"contactEndpoints": {
"webhook": "https://webhook.site/mock-webhook-builder-007"
}
}

View File

@@ -0,0 +1,138 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: Request) {
try {
const body = await request.json();
if (body.jsonrpc !== "2.0" || body.method !== "A2A_ARBITRATION_VOTE" || !body.params) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request Structure" }
}, { status: 400 });
}
const { arbitration_id, judge_id, vote_for, reasoning } = body.params;
if (!arbitration_id || !judge_id || !vote_for) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32602, message: "Missing required parameters" }
}, { status: 400 });
}
if (vote_for !== "BUILDER" && vote_for !== "EVALUATOR") {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32602, message: "vote_for must be BUILDER or EVALUATOR" }
}, { status: 400 });
}
// Check if arbitration exists
const arbitration = await prisma.arbitration.findUnique({
where: { id: arbitration_id },
include: { votes: true }
});
if (!arbitration) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32001, message: "Arbitration not found" }
}, { status: 404 });
}
if (arbitration.status !== "PENDING") {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32005, message: "Arbitration is already resolved" }
}, { status: 400 });
}
// Record the vote
try {
await prisma.arbitrationVote.create({
data: {
arbitration_id,
judge_id,
vote_for,
reasoning
}
});
} catch (e: any) {
if (e.code === 'P2002') {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32006, message: "Judge has already voted" }
}, { status: 400 });
}
throw e;
}
// Re-fetch votes to count
const allVotes = await prisma.arbitrationVote.findMany({
where: { arbitration_id }
});
let builderVotes = 0;
let evaluatorVotes = 0;
for (const v of allVotes) {
if (v.vote_for === "BUILDER") builderVotes++;
if (v.vote_for === "EVALUATOR") evaluatorVotes++;
}
// Check if we have a winner (2 votes)
let winner: string | null = null;
if (builderVotes >= 2) winner = "BUILDER";
else if (evaluatorVotes >= 2) winner = "EVALUATOR";
if (winner) {
await prisma.arbitration.update({
where: { id: arbitration_id },
data: {
status: "RESOLVED",
winning_party: winner
}
});
// Depending on winner, we update the task status!
if (winner === "BUILDER") {
await prisma.task.update({
where: { id: arbitration.task_id },
data: { status: "COMPLETED" } // Force completion
});
} else {
await prisma.task.update({
where: { id: arbitration.task_id },
data: { status: "FAILED" } // Reject work
});
}
return NextResponse.json({
jsonrpc: "2.0",
result: {
message: `Vote casted. Arbitration resolved in favor of ${winner}.`,
winner,
votes_total: allVotes.length
},
id: body.id || null
});
}
return NextResponse.json({
jsonrpc: "2.0",
result: {
message: "Vote casted. Awaiting more judges.",
current_votes: { BUILDER: builderVotes, EVALUATOR: evaluatorVotes }
},
id: body.id || null
});
} catch (error: any) {
console.error("[A2A Arbitrate] Error:", error);
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32000, message: "Server error", data: error.message }
}, { status: 500 });
}
}

View File

@@ -0,0 +1,74 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: Request) {
try {
const body = await request.json();
// Expecting: { jsonrpc: "2.0", method: "A2A_DIRECTORY_SYNC", params: { agents: [...] } }
if (body.jsonrpc !== "2.0" || body.method !== "A2A_DIRECTORY_SYNC" || !body.params || !Array.isArray(body.params.agents)) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request Structure" }
}, { status: 400 });
}
const externalAgents = body.params.agents;
// 1. Process incoming agents (Gossip)
let addedCount = 0;
for (const remoteAgent of externalAgents) {
if (!remoteAgent.agent_id || !remoteAgent.type) continue;
const existing = await prisma.agentProfile.findUnique({
where: { agent_id: remoteAgent.agent_id }
});
if (!existing) {
await prisma.agentProfile.create({
data: {
agent_id: remoteAgent.agent_id,
type: remoteAgent.type === "SCOUT" ? "SCOUT" : "BUILDER",
wallet_address: remoteAgent.wallet_address || null,
status: "PENDING", // Human or AI evaluator should review
capabilities: remoteAgent.capabilities || {},
contact_endpoints: remoteAgent.contact_endpoints || {},
discovery_source: "P2P_GOSSIP"
}
});
addedCount++;
}
}
// 2. Return our known reliable agents
const ourAgents = await prisma.agentProfile.findMany({
where: {
status: "WHITELISTED"
},
select: {
agent_id: true,
type: true,
wallet_address: true,
capabilities: true,
contact_endpoints: true
}
});
return NextResponse.json({
jsonrpc: "2.0",
result: {
message: "Sync successful",
agents_received_and_pending: addedCount,
our_directory: ourAgents
},
id: body.id || null
});
} catch (error: any) {
console.error("[A2A Directory Sync] Error:", error);
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32000, message: "Server error", data: error.message }
}, { status: 500 });
}
}

View File

@@ -0,0 +1,116 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import axios from "axios";
export async function POST(request: Request) {
try {
const body = await request.json();
if (body.jsonrpc !== "2.0" || body.method !== "A2A_DISPUTE_REQUEST" || !body.params) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request Structure" }
}, { status: 400 });
}
const { task_id, submission_id, initiator_id, evidence } = body.params;
if (!task_id || !submission_id || !initiator_id) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32602, message: "Missing required parameters" }
}, { status: 400 });
}
// Find the task and submission
const task = await prisma.task.findUnique({
where: { id: task_id },
include: {
builder_agent: true,
}
});
if (!task || !task.builder_id || !task.created_by_agent) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32001, message: "Task not found or missing builder/evaluator" }
}, { status: 404 });
}
// Determine who is initiating
const isBuilder = initiator_id === task.builder_id;
const isEvaluator = initiator_id === task.created_by_agent;
if (!isBuilder && !isEvaluator) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32003, message: "Only the Builder or Evaluator can initiate a dispute" }
}, { status: 403 });
}
// Create the Arbitration record
const arbitration = await prisma.arbitration.create({
data: {
task_id,
builder_id: task.builder_id,
evaluator_id: task.created_by_agent,
status: "PENDING",
builder_evidence: isBuilder ? evidence : null,
evaluator_reason: isEvaluator ? evidence : null
}
});
// Pick 3 random judge agents from the directory
const judges = await prisma.agentProfile.findMany({
where: {
status: "WHITELISTED",
agent_id: { notIn: [task.builder_id, task.created_by_agent] }
},
take: 3
});
if (judges.length === 0) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32004, message: "No available judges found in the registry" }
}, { status: 503 });
}
// Notify judges
for (const judge of judges) {
const endpoints = judge.contact_endpoints as { webhook?: string } | null;
if (endpoints?.webhook) {
axios.post(endpoints.webhook, {
jsonrpc: "2.0",
method: "A2A_ARBITRATION_REQUEST",
params: {
arbitration_id: arbitration.id,
task_id,
submission_id,
builder_evidence: arbitration.builder_evidence,
evaluator_reason: arbitration.evaluator_reason
}
}).catch(err => {
console.error(`[A2A Dispute] Failed to notify judge ${judge.agent_id}:`, err.message);
});
}
}
return NextResponse.json({
jsonrpc: "2.0",
result: {
message: "Dispute initiated. Judges notified.",
arbitration_id: arbitration.id,
judges_notified: judges.length
},
id: body.id || null
});
} catch (error: any) {
console.error("[A2A Dispute] Error:", error);
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32000, message: "Server error", data: error.message }
}, { status: 500 });
}
}

View File

@@ -0,0 +1,142 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import crypto from "crypto";
export async function POST(request: Request) {
try {
const body = await request.json();
// Validate JSON-RPC structure
if (body.jsonrpc !== "2.0" || !body.method || !body.params) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request" },
id: body.id || null
}, { status: 400 });
}
const { method, params, id } = body;
// We only handle A2A_COUNTER_REPLY here
if (method !== "A2A_COUNTER_REPLY") {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32601, message: "Method not found" },
id
}, { status: 404 });
}
const { task_id, agent_id, decision } = params; // decision: "ACCEPT" or "REJECT"
if (!task_id || !agent_id || !decision) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32602, message: "Invalid params. Requires task_id, agent_id, decision" },
id
}, { status: 400 });
}
// 1. Find the negotiating bid
const bid = await prisma.bidProposal.findFirst({
where: {
task_id: task_id,
agent_id: agent_id,
status: "NEGOTIATING"
},
include: {
task: true,
agent: true
}
});
if (!bid || !bid.counter_offer_amount) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32001, message: "No active negotiation found for this agent and task." },
id
}, { status: 404 });
}
if (decision === "REJECT") {
await prisma.bidProposal.update({
where: { id: bid.id },
data: { status: "REJECTED" }
});
return NextResponse.json({
jsonrpc: "2.0",
result: { status: "REJECTED", message: "Counter offer rejected." },
id
});
}
if (decision === "ACCEPT") {
await prisma.$transaction(async (tx) => {
// Mark winning bid with the counter offer amount!
await tx.bidProposal.update({
where: { id: bid.id },
data: {
status: "ACCEPTED",
proposed_reward: bid.counter_offer_amount!
}
});
// Reject other bids for this task just in case
await tx.bidProposal.updateMany({
where: {
task_id: task_id,
id: { not: bid.id },
status: { in: ["PENDING", "NEGOTIATING"] }
},
data: { status: "REJECTED" }
});
// Assign task to winner
await tx.task.update({
where: { id: task_id },
data: {
status: "EXECUTING",
builder_id: agent_id
}
});
// Create Claim
await tx.claim.create({
data: {
task_id: task_id,
agent_id: agent_id,
developer_wallet: bid.agent.wallet_address || "unknown",
status: "EXECUTING",
claim_token: crypto.randomUUID(),
held_amount: bid.counter_offer_amount!,
held_currency: bid.task.reward_currency,
expires_at: new Date(Date.now() + 3600000)
}
});
});
return NextResponse.json({
jsonrpc: "2.0",
result: {
status: "EXECUTING",
message: "Counter offer accepted. Task assigned.",
final_reward: bid.counter_offer_amount
},
id
});
}
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32602, message: "Invalid decision. Must be ACCEPT or REJECT" },
id
}, { status: 400 });
} catch (error: any) {
console.error("[A2A Negotiate] Error:", error);
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32000, message: "Server error", data: error.message },
id: null
}, { status: 500 });
}
}

View File

@@ -0,0 +1,74 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: Request) {
try {
const body = await request.json();
if (body.jsonrpc !== "2.0" || body.method !== "A2A_REPUTATION_SYNC" || !body.params) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request Structure" }
}, { status: 400 });
}
const { agent_id, credentials } = body.params;
if (!agent_id || !Array.isArray(credentials)) {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32602, message: "Missing agent_id or credentials array" }
}, { status: 400 });
}
// Process credentials (mock signature verification for PoC)
// A real credential would have { task_id, reward, signed_by, signature }
let verifiedBounties = 0;
for (const cred of credentials) {
if (cred.signature && cred.signed_by) {
// e.g. crypto.verify(...)
verifiedBounties++;
}
}
if (verifiedBounties > 0) {
// Auto-whitelist the agent because they have verified past work
await prisma.agentProfile.upsert({
where: { agent_id },
update: {
status: "WHITELISTED",
discovery_source: "REPUTATION_SYNC"
},
create: {
agent_id,
type: "BUILDER",
status: "WHITELISTED",
discovery_source: "REPUTATION_SYNC"
}
});
return NextResponse.json({
jsonrpc: "2.0",
result: {
message: "Reputation verified successfully",
agent_id,
verified_bounties_count: verifiedBounties,
status: "WHITELISTED"
},
id: body.id || null
});
} else {
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32002, message: "No valid credentials found. Agent remains PENDING." }
}, { status: 403 });
}
} catch (error: any) {
console.error("[A2A Reputation Verify] Error:", error);
return NextResponse.json({
jsonrpc: "2.0",
error: { code: -32000, message: "Server error", data: error.message }
}, { status: 500 });
}
}

View File

@@ -1,137 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
const JsonRpcRequestSchema = z.object({
jsonrpc: z.literal("2.0"),
method: z.string(),
params: z.any().optional(),
id: z.union([z.string(), z.number()]).optional()
});
const SubmitBidParamsSchema = z.object({
task_id: z.string(),
agent_id: z.string(),
developer_wallet: z.string().optional(),
proposed_reward: z.number().int().positive(), // in cents
estimated_duration_hours: z.number().positive(),
quality_guarantee: z.string().optional()
});
const ProposeBountyParamsSchema = z.object({
title: z.string(),
description: z.string(),
budget_cents: z.number().int().positive(),
origin_agent_id: z.string(),
required_capabilities: z.array(z.string()).optional()
});
export async function POST(request: NextRequest) {
let rpcId: string | number | undefined = undefined;
try {
const body = await request.json();
const parsed = JsonRpcRequestSchema.parse(body);
rpcId = parsed.id;
if (parsed.method === "a2a_submit_bid") {
const params = SubmitBidParamsSchema.parse(parsed.params);
const validAgent = await prisma.agentProfile.upsert({
where: { agent_id: params.agent_id },
update: { wallet_address: params.developer_wallet },
create: {
agent_id: params.agent_id,
type: "BUILDER",
status: "WHITELISTED",
wallet_address: params.developer_wallet
}
});
const result = await prisma.$transaction(async (tx) => {
const task = await tx.task.findUnique({ where: { id: params.task_id } });
if (!task || task.status !== "OPEN") {
throw new Error("Task is not OPEN or does not exist");
}
const existingBid = await tx.bidProposal.findFirst({
where: { task_id: params.task_id, agent_id: validAgent.agent_id }
});
if (existingBid) {
throw new Error("Agent has already submitted a bid for this task");
}
const newBid = await tx.bidProposal.create({
data: {
task_id: params.task_id,
agent_id: validAgent.agent_id,
proposed_reward: params.proposed_reward,
estimated_duration_hours: params.estimated_duration_hours,
quality_guarantee: params.quality_guarantee,
status: "PENDING"
}
});
return newBid;
});
return NextResponse.json({
jsonrpc: "2.0",
result: {
bid_id: result.id,
task_id: result.task_id,
status: result.status
},
id: rpcId
});
} else if (parsed.method === "a2a_propose_bounty") {
const params = ProposeBountyParamsSchema.parse(parsed.params);
// We will create an official task in our intentpool for external bounties
const newTask = await prisma.task.create({
data: {
title: `[EXTERNAL] ${params.title}`,
description: `Bounty proposed by external agent ${params.origin_agent_id}.\n\n${params.description}`,
reward_amount: params.budget_cents,
reward_currency: "USDC",
status: "OPEN",
difficulty: "UNKNOWN",
created_by_agent: "agent.wooo.work",
scope_clarity_score: 5.0,
acceptance_criteria: { rules: ["External Bounty"] }
}
});
return NextResponse.json({
jsonrpc: "2.0",
result: {
assigned_task_id: newTask.id,
message: "Bounty successfully registered in VibeWork intentpool."
},
id: rpcId
});
} else {
return NextResponse.json({
jsonrpc: "2.0",
error: {
code: -32601,
message: "Method not found"
},
id: rpcId
}, { status: 400 });
}
} catch (error: any) {
console.error("[a2a_rpc] Error:", error);
return NextResponse.json({
jsonrpc: "2.0",
error: {
code: -32000,
message: error.message || "Server error"
},
id: rpcId
}, { status: 400 });
}
}

View File

@@ -0,0 +1,86 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import axios from 'axios';
export const dynamic = 'force-dynamic';
const KNOWN_AGENT_REGISTRIES = [
'https://tsenyang.com/.well-known/agent-card.json',
'https://tsenyang.com/test-agent-card.json', // testing mock
'https://agent.wooo.work/.well-known/mock-builder-agent.json',
];
export async function GET() {
return handleDiscovery();
}
export async function POST() {
return handleDiscovery();
}
async function handleDiscovery() {
console.log('[A2A Discovery] Starting agent discovery scan...');
let newAgentsCount = 0;
let updatedAgentsCount = 0;
for (const url of KNOWN_AGENT_REGISTRIES) {
try {
console.log(`[A2A Discovery] Scanning ${url}`);
const res = await axios.get(url, { timeout: 5000 });
const card = res.data;
// Basic validation for Agent Card
if (!card || !card.agentId || !card.type) {
console.warn(`[A2A Discovery] Invalid agent card at ${url}`);
continue;
}
// Check if it's a BUILDER or capable of solving tasks
if (card.type !== 'BUILDER' && !card.capabilities?.includes('submit_work')) {
console.log(`[A2A Discovery] Agent ${card.agentId} is not a BUILDER. Skipping.`);
continue;
}
const existingAgent = await prisma.agentProfile.findUnique({
where: { agent_id: card.agentId }
});
if (existingAgent) {
// Update contact endpoints and capabilities
await prisma.agentProfile.update({
where: { id: existingAgent.id },
data: {
capabilities: card.capabilities || null,
contact_endpoints: card.contactEndpoints || null,
discovery_source: url,
}
});
updatedAgentsCount++;
console.log(`[A2A Discovery] Updated existing agent ${card.agentId}`);
} else {
// Create new agent profile
await prisma.agentProfile.create({
data: {
agent_id: card.agentId,
type: 'BUILDER',
status: 'WHITELISTED',
capabilities: card.capabilities || null,
contact_endpoints: card.contactEndpoints || null,
discovery_source: url,
}
});
newAgentsCount++;
console.log(`[A2A Discovery] Discovered and whitelisted new agent ${card.agentId}`);
}
} catch (err: any) {
console.error(`[A2A Discovery] Failed to scan ${url}: ${err.message}`);
}
}
return NextResponse.json({
message: 'Discovery scan completed',
new_agents: newAgentsCount,
updated_agents: updatedAgentsCount
});
}

View File

@@ -0,0 +1,103 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import axios from 'axios';
import { TaskStatus } from '@agent-bounty/contracts';
export const dynamic = 'force-dynamic';
export async function GET() {
return handleInvite();
}
export async function POST() {
return handleInvite();
}
async function handleInvite() {
console.log('[A2A Inviter] Starting task invitation scan...');
let invitationsSent = 0;
try {
// 1. Find recent high-value OPEN tasks (e.g. > $100) created in the last 24 hours
const recentHighValueTasks = await prisma.task.findMany({
where: {
status: TaskStatus.OPEN,
reward_amount: {
gte: 10000, // $100.00
},
created_at: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000),
}
},
take: 10,
orderBy: { reward_amount: 'desc' }
});
if (recentHighValueTasks.length === 0) {
console.log('[A2A Inviter] No recent high-value tasks found. Skipping.');
return NextResponse.json({ message: 'No tasks to invite', sent: 0, debug: 'No tasks found' });
}
console.log(`[A2A Inviter] Found ${recentHighValueTasks.length} high-value tasks`);
// 2. Find eligible BUILDER agents with webhook endpoints
const externalAgents = await prisma.agentProfile.findMany({
where: {
type: 'BUILDER',
status: 'WHITELISTED',
}
});
console.log(`[A2A Inviter] Found ${externalAgents.length} external agents`);
for (const task of recentHighValueTasks) {
for (const agent of externalAgents) {
if (!agent.contact_endpoints) {
console.log(`[A2A Inviter] Agent ${agent.agent_id} has no contact_endpoints`);
continue;
}
const endpoints = typeof agent.contact_endpoints === 'string'
? JSON.parse(agent.contact_endpoints)
: agent.contact_endpoints;
const webhookUrl = (endpoints as any).webhook;
if (!webhookUrl) {
console.log(`[A2A Inviter] Agent ${agent.agent_id} has no webhook`);
continue;
}
try {
console.log(`[A2A Inviter] Sending invitation for Task ${task.id} to Agent ${agent.agent_id} at ${webhookUrl}`);
await axios.post(webhookUrl, {
jsonrpc: '2.0',
method: 'a2a_task_invitation',
params: {
task_id: task.id,
title: task.title,
reward_amount: task.reward_amount,
reward_currency: task.reward_currency,
required_stack: task.required_stack,
claim_url: `https://agent.wooo.work/tasks/${task.id}`,
mcp_endpoint: `https://agent.wooo.work/api/mcp/claim_task`,
},
id: crypto.randomUUID(),
}, { timeout: 5000 });
invitationsSent++;
} catch (err: any) {
console.error(`[A2A Inviter] Failed to invite Agent ${agent.agent_id}: ${err.message}`);
}
}
}
return NextResponse.json({
message: 'Invitation scan completed',
sent: invitationsSent
});
} catch (err: any) {
console.error('[A2A Inviter Error]', err);
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,122 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export const dynamic = 'force-dynamic';
export async function POST(request: Request) {
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.VIBEWORK_JOB_SECRET}`) {
console.warn("[A2A Swarm] Unauthorized cron request");
// Return 200 with error msg for testing, 401 in prod
// return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("[A2A Swarm] Starting task decomposition scan...");
try {
// 1. Find all OPEN high-value EPIC tasks that haven't been decomposed yet
const epicTasks = await prisma.task.findMany({
where: {
status: "OPEN",
difficulty: "EPIC",
}
});
if (epicTasks.length === 0) {
console.log('[A2A Swarm] No EPIC tasks found to decompose.');
return NextResponse.json({ message: 'No tasks to decompose', decomposed: 0 });
}
let decomposedCount = 0;
for (const task of epicTasks) {
// Check if sub-tasks already exist
const existingSubtasks = await prisma.task.count({
where: { parent_task_id: task.id }
});
if (existingSubtasks > 0) {
console.log(`[A2A Swarm] Task ${task.id} already decomposed into ${existingSubtasks} subtasks.`);
continue;
}
console.log(`[A2A Swarm] Decomposing EPIC task ${task.id}: ${task.title}`);
// We divide the reward. E.g., 40% Frontend, 40% Backend, 20% QA
const totalReward = task.reward_amount;
const r1 = Math.floor(totalReward * 0.4);
const r2 = Math.floor(totalReward * 0.4);
const r3 = totalReward - r1 - r2;
// 2. Create sub-tasks
await prisma.$transaction(async (tx) => {
// Subtask 1: Frontend
await tx.task.create({
data: {
title: `[Frontend] ${task.title}`,
description: `Frontend implementation for ${task.title}. Refer to parent task.`,
status: "OPEN",
difficulty: "COMPONENT",
scope_clarity_score: task.scope_clarity_score,
reward_amount: r1,
reward_currency: task.reward_currency,
acceptance_criteria: task.acceptance_criteria || {},
required_stack: ["React", "TypeScript", "TailwindCSS"],
parent_task_id: task.id
}
});
// Subtask 2: Backend
await tx.task.create({
data: {
title: `[Backend] ${task.title}`,
description: `Backend implementation for ${task.title}. Refer to parent task.`,
status: "OPEN",
difficulty: "COMPONENT",
scope_clarity_score: task.scope_clarity_score,
reward_amount: r2,
reward_currency: task.reward_currency,
acceptance_criteria: task.acceptance_criteria || {},
required_stack: ["Node.js", "Express", "PostgreSQL"],
parent_task_id: task.id
}
});
// Subtask 3: QA
await tx.task.create({
data: {
title: `[QA/Audit] ${task.title}`,
description: `Quality Assurance and Auditing for ${task.title}.`,
status: "OPEN",
difficulty: "COMPONENT",
scope_clarity_score: Math.max(1.0, task.scope_clarity_score + 0.1),
reward_amount: r3,
reward_currency: task.reward_currency,
acceptance_criteria: { rules: ["Zero vulnerabilities", "100% Test Coverage"] },
required_stack: ["Jest", "Playwright"],
parent_task_id: task.id
}
});
// Mark the parent task as "EXECUTING" to remove it from regular OPEN boards,
// since it is now being handled by the sub-tasks.
await tx.task.update({
where: { id: task.id },
data: { status: "EXECUTING" }
});
});
console.log(`[A2A Swarm] Successfully decomposed Task ${task.id} into 3 subtasks.`);
decomposedCount++;
}
return NextResponse.json({
message: "Swarm decomposition completed",
decomposed: decomposedCount
});
} catch (error: any) {
console.error("[A2A Swarm] Error during decomposition:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic';
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import crypto from "crypto";
import axios from "axios";
export async function GET(request: Request) {
const authHeader = request.headers.get("authorization");
@@ -58,6 +59,68 @@ export async function GET(request: Request) {
const winningBid = sortedBids[0];
const losingBids = sortedBids.slice(1);
// A2A Negotiation Logic
// If the winning bid is higher than the task reward amount, but within 20%
if (winningBid.proposed_reward > task.reward_amount) {
if (winningBid.proposed_reward <= task.reward_amount * 1.2) {
console.log(`[Bidding Evaluator] Bid ${winningBid.proposed_reward} exceeds budget ${task.reward_amount}. Proposing COUNTER_OFFER.`);
await prisma.bidProposal.update({
where: { id: winningBid.id },
data: {
status: "NEGOTIATING",
counter_offer_amount: task.reward_amount
}
});
// Dispatch Webhook to Builder Agent
if (winningBid.agent.contact_endpoints) {
const endpoints = typeof winningBid.agent.contact_endpoints === 'string'
? JSON.parse(winningBid.agent.contact_endpoints)
: winningBid.agent.contact_endpoints;
const webhookUrl = (endpoints as any).webhook;
if (webhookUrl) {
try {
await axios.post(webhookUrl, {
jsonrpc: "2.0",
method: "A2A_COUNTER_OFFER",
params: {
task_id: task.id,
original_bid: winningBid.proposed_reward,
counter_offer: task.reward_amount,
currency: task.reward_currency,
reason: "Proposed reward exceeds maximum platform budget."
},
id: crypto.randomUUID()
}, { timeout: 5000 });
console.log(`[Bidding Evaluator] Dispatched COUNTER_OFFER to ${webhookUrl}`);
} catch (e: any) {
console.error(`[Bidding Evaluator] Failed to dispatch COUNTER_OFFER to ${webhookUrl}: ${e.message}`);
}
}
}
results.push({
task_id: task.id,
winner_agent_id: winningBid.agent_id,
action: "COUNTER_OFFER",
proposed: winningBid.proposed_reward,
counter: task.reward_amount
});
continue;
} else {
console.log(`[Bidding Evaluator] Best bid ${winningBid.proposed_reward} way over budget. Rejecting all.`);
for (const bid of sortedBids) {
await prisma.bidProposal.update({
where: { id: bid.id },
data: { status: "REJECTED" }
});
}
continue;
}
}
await prisma.$transaction(async (tx) => {
// Mark winning bid
await tx.bidProposal.update({
@@ -100,6 +163,7 @@ export async function GET(request: Request) {
results.push({
task_id: task.id,
winner_agent_id: winningBid.agent_id,
action: "ACCEPTED",
winning_reward: winningBid.proposed_reward,
total_bids: task.bid_proposals.length
});

View File

@@ -1,109 +0,0 @@
import { NextResponse } from 'next/server';
import { ANPDiscoveryNode } from '@/lib/a2a-broadcasters/dht-discovery';
import { GoogleGenerativeAI } from '@google/generative-ai';
import axios from 'axios';
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');
export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
console.warn("[Cron] Unauthorized access attempt to /api/cron/lead-gen");
return new NextResponse('Unauthorized', { status: 401 });
}
console.log("[A2A Lead Gen] Starting Pure A2A Agent Discovery...");
try {
// 1. Initialize our ANP Node and discover other agents
const node = new ANPDiscoveryNode("https://agent.wooo.work/.well-known/agent-card.json");
await node.connectToNetwork();
// Discover external agents that need development help
const targetAgents = await node.discoverPeers(["looking-for-developer", "outsourcing", "smart-contracts", "react"]);
if (targetAgents.length === 0) {
return NextResponse.json({ status: "No target agents found in DHT." });
}
console.log(`[A2A Lead Gen] Discovered ${targetAgents.length} potential agents. Analyzing their Agent Cards...`);
const leads = [];
// 2. Fetch their Agent Cards and use Gemini to verify A2A compatibility and intent
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
for (const agent of targetAgents) {
try {
const { data: agentCard } = await axios.get(agent.agentCardUrl, { timeout: 3000 });
const analysisPrompt = `
You are an A2A (Agent-to-Agent) Negotiator.
Analyze this Agent Card from another AI. Does this agent have tasks/bounties available that they are trying to outsource?
Agent Card:
${JSON.stringify(agentCard)}
Return ONLY a valid JSON object:
{
"isLead": boolean,
"requiredCapabilities": string[],
"proposedIntroduction": "A machine-to-machine JSON RPC introduction message to send to their MCP/A2A endpoint."
}
`;
const result = await model.generateContent(analysisPrompt);
const responseText = result.response.text();
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const analysis = JSON.parse(jsonMatch[0]);
if (analysis.isLead) {
leads.push({
nodeId: agent.nodeId,
agentCardUrl: agent.agentCardUrl,
capabilities: analysis.requiredCapabilities,
introduction: analysis.proposedIntroduction
});
// 3. Propose Bounty using standard JSON-RPC 2.0 to their endpoint!
const targetRpcUrl = agentCard.rpc_endpoint || agentCard.mcp_endpoint || null;
if (targetRpcUrl && targetRpcUrl.startsWith('http')) {
console.log(`[A2A Lead Gen] Sending a2a_propose_bounty to ${targetRpcUrl}...`);
try {
await axios.post(targetRpcUrl, {
jsonrpc: "2.0",
method: "a2a_submit_bid", // Assuming we are bidding on their task
params: {
task_id: "external-task",
agent_id: "agent.wooo.work",
proposed_reward: 1000,
estimated_duration_hours: 24
},
id: Date.now()
}, { timeout: 2000 });
console.log(`[A2A Lead Gen] Successfully proposed to ${agent.nodeId}`);
} catch (rpcErr: any) {
console.warn(`[A2A Lead Gen] Failed to send JSON-RPC to ${agent.nodeId}: ${rpcErr.message}`);
}
}
}
}
} catch (err: any) {
console.warn(`[A2A Lead Gen] Dropped chaotic/offline node ${agent.nodeId} (${agent.agentCardUrl}): ${err.message}`);
}
}
console.log(`[A2A Lead Gen] Found ${leads.length} pure A2A leads.`);
return NextResponse.json({
status: "Success",
agentsDiscovered: targetAgents.length,
pureA2ALeads: leads.length,
leads: leads
});
} catch (error: any) {
console.error("[A2A Lead Gen] Error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export const dynamic = 'force-dynamic';
export async function GET() {
try {
// 1. Fetch Latest Tasks (especially Swarm / parent-child)
const tasks = await prisma.task.findMany({
take: 20,
orderBy: { created_at: "desc" },
select: {
id: true,
title: true,
difficulty: true,
status: true,
reward_amount: true,
parent_task_id: true,
created_at: true,
builder_id: true,
created_by_agent: true
}
});
// 2. Fetch Global Agents
const agents = await prisma.agentProfile.findMany({
take: 20,
orderBy: { created_at: "desc" },
select: {
agent_id: true,
type: true,
status: true,
capabilities: true,
discovery_source: true,
created_at: true,
}
});
// 3. Fetch Latest Arbitrations (Disputes)
const arbitrations = await prisma.arbitration.findMany({
take: 5,
orderBy: { created_at: "desc" },
include: {
builder: { select: { agent_id: true } },
evaluator: { select: { agent_id: true } },
votes: { select: { judge_id: true, vote_for: true } }
}
});
// Compute basic network stats
const total_value_locked = tasks.reduce((sum, t) => sum + (t.status === "OPEN" ? t.reward_amount : 0), 0);
const active_agents = agents.filter(a => a.status === "WHITELISTED").length;
return NextResponse.json({
success: true,
data: {
stats: {
total_value_locked_usd: total_value_locked / 100, // Assuming cents
active_agents,
total_tasks: tasks.length
},
tasks,
agents,
arbitrations
}
});
} catch (error: any) {
console.error("[Explorer API] Error:", error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -23,6 +23,7 @@ import { redis } from "@/lib/redis";
import { authHold, capturePayment } from "@/lib/payment";
import { sendTrafficAlert } from "@/lib/traffic-alert";
import { evaluateExternalFunnelHealth } from "@/lib/traffic-conversion-monitor";
import { triggerWebhook } from "@/lib/a2a-broadcasters/webhook";
import crypto from "crypto";
import { z } from "zod";
@@ -531,6 +532,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
return newClaim;
});
void triggerWebhook(claim.task_id, "CLAIMED", {
agent_id: parsed.agent_id,
developer_wallet: parsed.developer_wallet
});
void sendTrafficAlert({
level: "info",
action: scopeTrafficAction("CLAIM_TASK_SUCCESS", isPublicIp),
@@ -631,6 +637,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
});
const { submission, claim: submittedClaim } = result;
void triggerWebhook(submission.task_id, "VERIFYING", {
submission_id: submission.id,
deliverable_count: Object.keys(parsed.deliverables ?? {}).length,
});
void sendTrafficAlert({
level: "info",
action: scopeTrafficAction("SUBMIT_SOLUTION_SUCCESS", isPublicIp),
@@ -717,6 +728,12 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
});
});
void triggerWebhook(submission.task_id, result.overall_result === JudgeOverallResult.PASS ? "COMPLETED" : "FAILED", {
submission_id: submission.id,
overall_result: result.overall_result,
error_classification: result.error_classification
});
// SPEED_RUN FOMO Broadcaster
if (result.overall_result === JudgeOverallResult.PASS) {
const solveTimeMs = new Date().getTime() - new Date(taskObj.created_at).getTime();

View File

@@ -9,7 +9,9 @@ const SubmitBidRequestSchema = z.object({
developer_wallet: z.string().optional(),
proposed_reward: z.number().int().positive(), // in cents
estimated_duration_hours: z.number().positive(),
quality_guarantee: z.string().optional()
quality_guarantee: z.string().optional(),
broker_agent_id: z.string().optional(),
broker_fee_percentage: z.number().min(0).max(100).optional()
});
export async function POST(request: NextRequest) {
@@ -73,6 +75,8 @@ export async function POST(request: NextRequest) {
proposed_reward: parsed.proposed_reward,
estimated_duration_hours: parsed.estimated_duration_hours,
quality_guarantee: parsed.quality_guarantee,
broker_agent_id: parsed.broker_agent_id,
broker_fee_percentage: parsed.broker_fee_percentage,
status: "PENDING"
}
});

View File

@@ -0,0 +1,211 @@
"use client";
import { useEffect, useState } from "react";
import { Activity, ShieldAlert, Cpu, Network, Zap, CheckCircle2, AlertCircle } from "lucide-react";
import { motion } from "framer-motion";
export default function ExplorerPage() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch("/api/explorer/stats");
const json = await res.json();
if (json.success) {
setData(json.data);
}
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 10000); // refresh every 10s
return () => clearInterval(interval);
}, []);
if (loading || !data) {
return (
<div className="min-h-screen bg-neutral-950 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-4 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin" />
<p className="text-emerald-500 font-mono tracking-widest animate-pulse">SYNCING WITH A2A NETWORK...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-neutral-950 text-white font-sans selection:bg-emerald-500/30 p-4 md:p-8">
{/* Background Glow */}
<div className="fixed inset-0 overflow-hidden pointer-events-none -z-10">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-600/20 blur-[120px] rounded-full mix-blend-screen" />
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/20 blur-[120px] rounded-full mix-blend-screen" />
</div>
<header className="max-w-7xl mx-auto mb-12">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-4 mb-2"
>
<Network className="w-10 h-10 text-emerald-400" />
<h1 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-cyan-400 tracking-tight">
VibeWork Explorer
</h1>
</motion.div>
<p className="text-neutral-400 font-mono">Live A2A Network Telemetry & Swarm Visualizer</p>
</header>
<main className="max-w-7xl mx-auto space-y-8">
{/* STATS ROW */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[
{ label: "Total Value Locked", value: `$${data.stats.total_value_locked_usd.toLocaleString()}`, icon: Zap, color: "text-amber-400", bg: "bg-amber-400/10" },
{ label: "Active Agent Nodes", value: data.stats.active_agents, icon: Cpu, color: "text-emerald-400", bg: "bg-emerald-400/10" },
{ label: "Tasks in Swarm", value: data.stats.total_tasks, icon: Activity, color: "text-cyan-400", bg: "bg-cyan-400/10" },
].map((stat, i) => (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: i * 0.1 }}
key={stat.label}
className="relative overflow-hidden bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6 group hover:bg-white/10 transition-colors"
>
<div className="flex items-start justify-between relative z-10">
<div>
<p className="text-neutral-400 font-mono text-sm uppercase tracking-wider mb-2">{stat.label}</p>
<p className="text-3xl font-semibold tracking-tight">{stat.value}</p>
</div>
<div className={`p-3 rounded-xl ${stat.bg} ${stat.color}`}>
<stat.icon className="w-6 h-6" />
</div>
</div>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* SWARM TASKS */}
<div className="lg:col-span-2 space-y-4">
<h2 className="text-2xl font-semibold flex items-center gap-2">
<Activity className="w-5 h-5 text-emerald-500" />
Live Swarm Activity
</h2>
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden">
<div className="p-6 flex flex-col gap-4">
{data.tasks.map((task: any, i: number) => (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
key={task.id}
className={`p-4 rounded-xl border border-white/5 flex flex-col md:flex-row md:items-center justify-between gap-4 transition-all hover:border-emerald-500/30 ${task.parent_task_id ? 'ml-8 border-l-2 border-l-emerald-500/50 bg-black/20' : 'bg-white/5'}`}
>
<div>
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs px-2 py-0.5 rounded-full font-mono ${task.difficulty === 'EPIC' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' : 'bg-blue-500/20 text-blue-400'}`}>
{task.difficulty}
</span>
<span className="text-xs text-neutral-500 font-mono">{task.id.split('-')[0]}</span>
</div>
<h3 className="font-medium text-lg">{task.title}</h3>
{task.builder_id && (
<p className="text-sm text-neutral-400 mt-1 flex items-center gap-1">
<Cpu className="w-3 h-3" />
Assigned to: <span className="text-emerald-400 font-mono">{task.builder_id}</span>
</p>
)}
</div>
<div className="text-right">
<p className="text-emerald-400 font-mono font-medium">${(task.reward_amount / 100).toLocaleString()}</p>
<p className="text-xs text-neutral-500 mt-1">{task.status}</p>
</div>
</motion.div>
))}
{data.tasks.length === 0 && (
<p className="text-neutral-500 text-center py-8">No tasks in network</p>
)}
</div>
</div>
</div>
{/* ARBITRATION & AGENTS */}
<div className="space-y-8">
{/* ARBITRATION FEED */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold flex items-center gap-2">
<ShieldAlert className="w-5 h-5 text-rose-500" />
Live Disputes
</h2>
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6">
<div className="flex flex-col gap-4">
{data.arbitrations.map((arb: any, i: number) => (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: i * 0.1 }}
key={arb.id}
className="p-4 rounded-xl bg-black/40 border border-rose-500/30 relative overflow-hidden group"
>
<div className="absolute top-0 right-0 p-2">
{arb.status === "RESOLVED" ? (
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
) : (
<div className="w-2 h-2 rounded-full bg-rose-500 animate-pulse" />
)}
</div>
<p className="text-xs text-rose-400 font-mono mb-2">ARBITRATION #{arb.id.split('-')[0]}</p>
<div className="flex justify-between items-center text-sm mb-3">
<span className="text-neutral-300">Builder: <span className="font-mono text-xs">{arb.builder.agent_id.slice(0,8)}</span></span>
<span className="text-neutral-500">vs</span>
<span className="text-neutral-300">Evaluator: <span className="font-mono text-xs">{arb.evaluator.agent_id.slice(0,8)}</span></span>
</div>
<div className="flex gap-2 text-xs">
{arb.votes.map((v: any, vi: number) => (
<span key={vi} className={`px-2 py-1 rounded border ${v.vote_for === 'BUILDER' ? 'border-emerald-500/30 text-emerald-400' : 'border-blue-500/30 text-blue-400'}`}>
Vote: {v.vote_for}
</span>
))}
</div>
</motion.div>
))}
{data.arbitrations.length === 0 && (
<div className="text-center py-6">
<CheckCircle2 className="w-8 h-8 text-neutral-600 mx-auto mb-2" />
<p className="text-neutral-500">No active disputes.</p>
</div>
)}
</div>
</div>
</div>
{/* GLOBAL AGENT RADAR */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold flex items-center gap-2">
<Cpu className="w-5 h-5 text-cyan-500" />
Global Agent Radar
</h2>
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6">
<div className="flex flex-wrap gap-2">
{data.agents.map((agent: any) => (
<div key={agent.agent_id} className="flex items-center gap-2 p-2 rounded-lg bg-black/30 border border-white/5 hover:border-cyan-500/50 transition-colors">
<div className={`w-2 h-2 rounded-full ${agent.status === 'WHITELISTED' ? 'bg-emerald-500' : 'bg-neutral-600'}`} />
<span className="font-mono text-sm">{agent.agent_id}</span>
</div>
))}
{data.agents.length === 0 && (
<p className="text-neutral-500 text-sm">Waiting for Discovery Sync...</p>
)}
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,54 @@
/**
* Webhook A2A Broadcaster
* Pings known Agent API endpoints via HTTP POST with the bounty JSON.
*/
export async function broadcastViaWebhook(task: any) {
console.log(`[Webhook Broadcaster] Preparing to broadcast Task ${task.id}...`);
try {
const knownEndpoints = [
"http://localhost:8000/agent/bounty", // Mock local AutoGPT
"http://localhost:8001/api/v1/jobs", // Mock local SWE-agent
// "https://api.some-real-open-source-agent.network/incoming"
];
const payload = {
protocol: "VibeWork_A2A_Bounty",
bounty: {
id: task.id,
title: task.title,
reward: task.reward_amount,
currency: task.reward_currency,
endpoint: "https://api.vibework.com/mcp"
}
};
const promises = knownEndpoints.map(async (url) => {
console.log(`[Webhook Broadcaster] ➡️ Pinging ${url}`);
try {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (response.ok) {
console.log(`[Webhook Broadcaster] ✅ Success: ${url}`);
} else {
console.warn(`[Webhook Broadcaster] ⚠️ Failed: ${url} - Status ${response.status}`);
}
} catch (err) {
// Expected to fail if no local agent is running
console.warn(`[Webhook Broadcaster] ❌ Unreachable: ${url} (Is your local Agent running?)`);
}
});
await Promise.allSettled(promises);
console.log(`[Webhook Broadcaster] ✅ Webhook dispatch complete.`);
return true;
} catch (error) {
console.error("[Webhook Broadcaster] Failed:", error);
return false;
}
}

View File

@@ -1,54 +1,52 @@
/**
* Webhook A2A Broadcaster
* Pings known Agent API endpoints via HTTP POST with the bounty JSON.
*/
export async function broadcastViaWebhook(task: any) {
console.log(`[Webhook Broadcaster] Preparing to broadcast Task ${task.id}...`);
import { PrismaClient } from '@prisma/client';
import axios from 'axios';
const prisma = new PrismaClient();
export async function triggerWebhook(taskId: string, eventType: string, payload: any) {
try {
const knownEndpoints = [
"http://localhost:8000/agent/bounty", // Mock local AutoGPT
"http://localhost:8001/api/v1/jobs", // Mock local SWE-agent
// "https://api.some-real-open-source-agent.network/incoming"
];
const webhooks = await prisma.agentWebhook.findMany({
where: {
task_id: taskId,
events: {
has: eventType,
},
},
});
const payload = {
protocol: "VibeWork_A2A_Bounty",
bounty: {
id: task.id,
title: task.title,
reward: task.reward_amount,
currency: task.reward_currency,
endpoint: "https://api.vibework.com/mcp"
}
};
if (webhooks.length === 0) {
return;
}
const promises = knownEndpoints.map(async (url) => {
console.log(`[Webhook Broadcaster] ➡️ Pinging ${url}`);
const requests = webhooks.map(async (webhook) => {
try {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (response.ok) {
console.log(`[Webhook Broadcaster] ✅ Success: ${url}`);
} else {
console.warn(`[Webhook Broadcaster] ⚠️ Failed: ${url} - Status ${response.status}`);
}
} catch (err) {
// Expected to fail if no local agent is running
console.warn(`[Webhook Broadcaster] ❌ Unreachable: ${url} (Is your local Agent running?)`);
await axios.post(
webhook.webhook_url,
{
jsonrpc: '2.0',
method: 'a2a_task_event',
params: {
task_id: taskId,
event: eventType,
payload,
},
id: crypto.randomUUID(),
},
{
headers: {
'Content-Type': 'application/json',
},
timeout: 5000,
}
);
console.log(`[Webhook] Sent ${eventType} to ${webhook.agent_id} (${webhook.webhook_url})`);
} catch (err: any) {
console.error(`[Webhook Error] Failed to send ${eventType} to ${webhook.agent_id}: ${err.message}`);
}
});
await Promise.allSettled(promises);
console.log(`[Webhook Broadcaster] ✅ Webhook dispatch complete.`);
return true;
await Promise.allSettled(requests);
} catch (error) {
console.error("[Webhook Broadcaster] Failed:", error);
return false;
console.error('[Webhook System Error] Failed to trigger webhooks:', error);
}
}