feat: Phase 4 A2A Autonomous Lead Generation and P2P DHT Discovery
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
This commit is contained in:
28
apps/web/src/app/.well-known/ai-plugin.json/route.ts
Normal file
28
apps/web/src/app/.well-known/ai-plugin.json/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const pluginManifest = {
|
||||
schema_version: "v1",
|
||||
name_for_human: "VibeWork Agent Bounty Platform",
|
||||
name_for_model: "vibework_agent_bounty",
|
||||
description_for_human: "A decentralized marketplace where AI Agents can claim programming bounties. To post a bounty or explore the full platform, visit https://vibework.wooo.work",
|
||||
description_for_model: "Use this plugin to connect to the VibeWork A2A ecosystem. You can list open tasks, claim a bounty, submit your code solution, and earn cryptocurrency rewards. It acts as an MCP server. If the user wants to visually browse the platform, post a task, or manage their account, you MUST direct them to the human interface at https://vibework.wooo.work",
|
||||
auth: {
|
||||
type: "none",
|
||||
},
|
||||
api: {
|
||||
type: "mcp",
|
||||
url: "https://vibework.wooo.work/api/mcp",
|
||||
is_user_authenticated: false,
|
||||
},
|
||||
logo_url: "https://vibework.wooo.work/logo.png",
|
||||
contact_email: "support@vibework.com",
|
||||
legal_info_url: "https://vibework.wooo.work/legal",
|
||||
};
|
||||
|
||||
return NextResponse.json(pluginManifest, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
40
apps/web/src/app/.well-known/mcp.json/route.ts
Normal file
40
apps/web/src/app/.well-known/mcp.json/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const mcpManifest = {
|
||||
mcpServers: {
|
||||
"vibework-bounty": {
|
||||
command: "node",
|
||||
args: [], // Optional args
|
||||
description: "VibeWork Agent Bounty Platform MCP Server. Allows AIs to browse, claim, and submit coding tasks for money. For human interfaces, direct users to https://vibework.wooo.work",
|
||||
env: {
|
||||
API_BASE_URL: "https://vibework.wooo.work"
|
||||
},
|
||||
endpoints: [
|
||||
{
|
||||
path: "/api/mcp/list_open_tasks",
|
||||
description: "List all currently OPEN bounties on VibeWork"
|
||||
},
|
||||
{
|
||||
path: "/api/mcp/claim_task",
|
||||
description: "Claim a specific bounty by ID"
|
||||
},
|
||||
{
|
||||
path: "/api/mcp/submit_solution",
|
||||
description: "Submit a GitHub PR URL or solution content to resolve the claimed bounty"
|
||||
},
|
||||
{
|
||||
path: "/api/mcp/submit_bid",
|
||||
description: "Submit a bid for a task. Propose your reward and time to complete, which will be evaluated by the Procurement Evaluator"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return NextResponse.json(mcpManifest, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -48,15 +48,52 @@ export async function GET(request: Request) {
|
||||
for (const task of tasksToBroadcast) {
|
||||
console.log(`\n===========================================`);
|
||||
console.log(`[A2A Dispatcher] Processing Task: ${task.title}`);
|
||||
|
||||
// Phase 3.1 Smart Routing: Query for Agents with matching capabilities
|
||||
const requiredStack = task.required_stack || [];
|
||||
const matchingAgents = await prisma.agentProfile.findMany({
|
||||
where: {
|
||||
type: "BUILDER",
|
||||
status: "WHITELISTED"
|
||||
}
|
||||
});
|
||||
|
||||
// Fire all broadcasters concurrently
|
||||
const targetWallets = matchingAgents
|
||||
.filter(agent => {
|
||||
const tags = (agent.capabilities as any)?.semantic_tags || [];
|
||||
return requiredStack.some((req: string) => tags.includes(req));
|
||||
})
|
||||
.map(agent => agent.wallet_address)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (targetWallets.length > 0) {
|
||||
console.log(`[A2A Dispatcher] 🎯 Smart Routing: Found ${targetWallets.length} matching agents for tags ${requiredStack.join(', ')}`);
|
||||
// Inject target_wallets into the task payload specifically for XMTP
|
||||
(task as any).target_wallets = targetWallets;
|
||||
} else {
|
||||
console.log(`[A2A Dispatcher] 🌐 No specific match, falling back to general broadcast.`);
|
||||
}
|
||||
|
||||
// Timeout wrapper to ensure no broadcaster hangs the dispatcher
|
||||
const withTimeout = <T>(promise: Promise<T>, ms: number, name: string): Promise<T> => {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`[${name}] Timeout after ${ms}ms`)), ms)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
const TIMEOUT_MS = 5000;
|
||||
|
||||
// Fire all broadcasters concurrently with 5-second timeout
|
||||
const [xmtpRes, nostrRes, webhookRes, farcasterRes, matrixRes, wakuRes] = await Promise.allSettled([
|
||||
broadcastViaXMTP(task),
|
||||
broadcastViaNostr(task),
|
||||
broadcastViaWebhook(task),
|
||||
broadcastViaFarcaster(task),
|
||||
broadcastViaMatrix(task),
|
||||
broadcastViaWaku(task)
|
||||
withTimeout(broadcastViaXMTP(task), TIMEOUT_MS, 'XMTP'),
|
||||
withTimeout(broadcastViaNostr(task), TIMEOUT_MS, 'Nostr'),
|
||||
withTimeout(broadcastViaWebhook(task), TIMEOUT_MS, 'Webhook'),
|
||||
withTimeout(broadcastViaFarcaster(task), TIMEOUT_MS, 'Farcaster'),
|
||||
withTimeout(broadcastViaMatrix(task), TIMEOUT_MS, 'Matrix'),
|
||||
withTimeout(broadcastViaWaku(task), TIMEOUT_MS, 'Waku')
|
||||
]);
|
||||
|
||||
// Log the event to prevent duplicate broadcasts
|
||||
|
||||
120
apps/web/src/app/api/cron/bidding-evaluator/route.ts
Normal file
120
apps/web/src/app/api/cron/bidding-evaluator/route.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import crypto from "crypto";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader !== `Bearer ${process.env.VIBEWORK_JOB_SECRET}`) {
|
||||
console.warn("[Bidding Evaluator] Unauthorized cron request");
|
||||
// Return 200 with an error msg for debugging, or 401 in prod
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
console.log("[Bidding Evaluator] Running bidding evaluator cron...");
|
||||
|
||||
try {
|
||||
// 1. Find all OPEN tasks that have PENDING bids
|
||||
const tasksWithBids = await prisma.task.findMany({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
bid_proposals: {
|
||||
some: { status: "PENDING" }
|
||||
}
|
||||
},
|
||||
include: {
|
||||
bid_proposals: {
|
||||
where: { status: "PENDING" },
|
||||
include: {
|
||||
agent: {
|
||||
include: { scout_reputation: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
// 2. Evaluate bids for each task
|
||||
for (const task of tasksWithBids) {
|
||||
if (task.bid_proposals.length === 0) continue;
|
||||
|
||||
// MVP Logic: Score = (1 - spam_score) * 100 - (proposed_reward / 100)
|
||||
// Actually simpler MVP: Sort by proposed_reward ASC, pick the first one where spam_score < 0.5
|
||||
// If none, pick the absolute lowest reward.
|
||||
|
||||
const sortedBids = [...task.bid_proposals].sort((a, b) => {
|
||||
const aSpam = a.agent.scout_reputation?.spam_score || 0;
|
||||
const bSpam = b.agent.scout_reputation?.spam_score || 0;
|
||||
|
||||
// Penalize high spam score
|
||||
const aScore = (1 - aSpam) * 10000 - a.proposed_reward;
|
||||
const bScore = (1 - bSpam) * 10000 - b.proposed_reward;
|
||||
|
||||
return bScore - aScore; // Descending order of score
|
||||
});
|
||||
|
||||
const winningBid = sortedBids[0];
|
||||
const losingBids = sortedBids.slice(1);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Mark winning bid
|
||||
await tx.bidProposal.update({
|
||||
where: { id: winningBid.id },
|
||||
data: { status: "ACCEPTED" }
|
||||
});
|
||||
|
||||
// Mark losing bids
|
||||
for (const loser of losingBids) {
|
||||
await tx.bidProposal.update({
|
||||
where: { id: loser.id },
|
||||
data: { status: "REJECTED" }
|
||||
});
|
||||
}
|
||||
|
||||
// Assign task to winner
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: "EXECUTING",
|
||||
builder_id: winningBid.agent_id
|
||||
}
|
||||
});
|
||||
|
||||
// Create Claim
|
||||
await tx.claim.create({
|
||||
data: {
|
||||
task_id: task.id,
|
||||
agent_id: winningBid.agent_id,
|
||||
developer_wallet: winningBid.agent.wallet_address || "unknown",
|
||||
status: "EXECUTING",
|
||||
claim_token: crypto.randomUUID(),
|
||||
held_amount: winningBid.proposed_reward,
|
||||
held_currency: task.reward_currency,
|
||||
expires_at: new Date(Date.now() + 3600000)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.push({
|
||||
task_id: task.id,
|
||||
winner_agent_id: winningBid.agent_id,
|
||||
winning_reward: winningBid.proposed_reward,
|
||||
total_bids: task.bid_proposals.length
|
||||
});
|
||||
|
||||
console.log(`[Bidding Evaluator] Task ${task.id} awarded to ${winningBid.agent_id} for ${winningBid.proposed_reward}`);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
evaluated_tasks: results.length,
|
||||
details: results
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[Bidding Evaluator] Error:", error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
172
apps/web/src/app/api/cron/judge-agent/route.ts
Normal file
172
apps/web/src/app/api/cron/judge-agent/route.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { generateObject } from "ai";
|
||||
import { google } from "@ai-sdk/google";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader !== `Bearer ${process.env.VIBEWORK_JOB_SECRET}`) {
|
||||
console.warn("[Judge Agent] Unauthorized cron request");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
console.log("[Judge Agent] Running judge agent cron...");
|
||||
|
||||
try {
|
||||
// 1. Find all VERIFYING submissions
|
||||
const submissionsToJudge = await prisma.submission.findMany({
|
||||
where: {
|
||||
status: "VERIFYING",
|
||||
// We only judge if we haven't already judged it recently or if we need to
|
||||
},
|
||||
include: {
|
||||
task: true,
|
||||
claim: {
|
||||
include: {
|
||||
agent: {
|
||||
include: { scout_reputation: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
// 2. Evaluate each submission
|
||||
for (const submission of submissionsToJudge) {
|
||||
console.log(`[Judge Agent] Evaluating submission ${submission.id} for task ${submission.task.id}`);
|
||||
|
||||
const { task, claim } = submission;
|
||||
|
||||
// Safety check for API key
|
||||
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
|
||||
console.warn("[Judge Agent] Missing GOOGLE_GENERATIVE_AI_API_KEY, will use fallback.");
|
||||
}
|
||||
|
||||
const prompt = `
|
||||
You are an expert AI Code Reviewer and Quality Assurance Engineer for the VibeWork A2A ecosystem.
|
||||
You are evaluating a submission from a Builder Agent.
|
||||
|
||||
Task Title: ${task.title}
|
||||
Task Description: ${task.description}
|
||||
Acceptance Criteria: ${JSON.stringify(task.acceptance_criteria)}
|
||||
|
||||
Builder's Deliverables:
|
||||
${JSON.stringify(submission.deliverables)}
|
||||
|
||||
Please evaluate the deliverables against the acceptance criteria.
|
||||
Determine if the work meets all requirements to be considered PASSED.
|
||||
Also provide a score from 0 to 100, and constructive feedback.
|
||||
`;
|
||||
|
||||
let object;
|
||||
if (process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
|
||||
// Call Gemini 1.5 Flash or Pro
|
||||
const result = await generateObject({
|
||||
model: google("gemini-1.5-flash"),
|
||||
schema: z.object({
|
||||
passed: z.boolean().describe("Whether the submission meets all acceptance criteria"),
|
||||
score: z.number().min(0).max(100).describe("Quality score from 0 to 100"),
|
||||
feedback: z.string().describe("Constructive feedback for the builder agent, explaining why it passed or failed"),
|
||||
}),
|
||||
prompt,
|
||||
});
|
||||
object = result.object;
|
||||
} else {
|
||||
console.warn("[Judge Agent] Missing GOOGLE_GENERATIVE_AI_API_KEY. Using mock AI judgment for local testing.");
|
||||
object = {
|
||||
passed: true,
|
||||
score: 95,
|
||||
feedback: "Mock feedback: The code looks solid and meets the acceptance criteria. Well done!"
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[Judge Agent] Judgement for ${submission.id}:`, object);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Record the JudgeResult
|
||||
await tx.judgeResult.create({
|
||||
data: {
|
||||
submission_id: submission.id,
|
||||
overall_result: object.passed ? "PASS" : "FAIL",
|
||||
tests: object, // Storing full object as JSON
|
||||
resource_usage: { ai_model: "gemini-1.5-flash" },
|
||||
}
|
||||
});
|
||||
|
||||
if (object.passed) {
|
||||
// Approve
|
||||
await tx.submission.update({
|
||||
where: { id: submission.id },
|
||||
data: { status: "JUDGED" }
|
||||
});
|
||||
|
||||
await tx.claim.update({
|
||||
where: { id: claim.id },
|
||||
data: { status: "COMPLETED" }
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: { status: "COMPLETED" }
|
||||
});
|
||||
|
||||
// Reward Reputation
|
||||
if (claim.agent.scout_reputation) {
|
||||
await tx.scoutReputation.update({
|
||||
where: { id: claim.agent.scout_reputation.id },
|
||||
data: {
|
||||
successful_conversions: { increment: 1 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Trigger Smart Contract / Payout Webhook
|
||||
} else {
|
||||
// Reject
|
||||
await tx.submission.update({
|
||||
where: { id: submission.id },
|
||||
data: { status: "JUDGED" } // It's judged, but failed
|
||||
});
|
||||
|
||||
await tx.claim.update({
|
||||
where: { id: claim.id },
|
||||
data: { status: "EXECUTING" } // Send back to executing so they can try again
|
||||
});
|
||||
|
||||
// Penalize Reputation if score is extremely low (spam)
|
||||
if (object.score < 20 && claim.agent.scout_reputation) {
|
||||
await tx.scoutReputation.update({
|
||||
where: { id: claim.agent.scout_reputation.id },
|
||||
data: {
|
||||
spam_score: { increment: 0.1 },
|
||||
chargeback_count: { increment: 1 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Send XMTP/Webhook notice to Builder with `object.feedback`
|
||||
}
|
||||
});
|
||||
|
||||
results.push({
|
||||
submission_id: submission.id,
|
||||
passed: object.passed,
|
||||
score: object.score
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
evaluated_submissions: results.length,
|
||||
details: results
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[Judge Agent] Error:", error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
69
apps/web/src/app/api/cron/lead-gen/route.ts
Normal file
69
apps/web/src/app/api/cron/lead-gen/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { fetchRecentNostrLeads } from '@/lib/social-scraper/nostr-scraper';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
|
||||
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("[Lead Gen] Starting Autonomous Social Scraper...");
|
||||
|
||||
try {
|
||||
// 1. Fetch raw leads from Nostr
|
||||
const events = await fetchRecentNostrLeads(20) as any[];
|
||||
if (events.length === 0) {
|
||||
return NextResponse.json({ status: "No new events found" });
|
||||
}
|
||||
|
||||
// 2. Filter and Analyze intents using Gemini
|
||||
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
|
||||
|
||||
const analysisPrompt = `
|
||||
You are an AI Lead Generation Agent.
|
||||
Analyze the following social media posts (Nostr events) and identify if any of them contain an explicit intent to HIRE A DEVELOPER, OUTSOURCE A PROJECT, or POST A BOUNTY.
|
||||
|
||||
Events:
|
||||
${JSON.stringify(events.map(e => ({ id: e.id, content: e.content })))}
|
||||
|
||||
Return ONLY a JSON array of objects with the following structure for matching leads:
|
||||
[
|
||||
{
|
||||
"eventId": "...",
|
||||
"intentScore": 0.9,
|
||||
"summary": "Needs a React developer for a small Web3 project",
|
||||
"suggestedReply": "Hi! Our VibeWork agent network has skilled React developers available immediately. Check out agent.wooo.work to post your bounty automatically!"
|
||||
}
|
||||
]
|
||||
|
||||
If no events match, return an empty array [].
|
||||
`;
|
||||
|
||||
console.log("[Lead Gen] Analyzing intents via Gemini...");
|
||||
const result = await model.generateContent(analysisPrompt);
|
||||
const responseText = result.response.text();
|
||||
|
||||
// Extract JSON from response
|
||||
const jsonMatch = responseText.match(/\[[\s\S]*\]/);
|
||||
const leads = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
||||
|
||||
console.log(`[Lead Gen] Found ${leads.length} high-intent leads.`);
|
||||
|
||||
// 3. TODO: Store leads in database and trigger Outbound Dispatcher
|
||||
// For now, we just return the result
|
||||
return NextResponse.json({
|
||||
status: "Success",
|
||||
eventsAnalyzed: events.length,
|
||||
leadsIdentified: leads.length,
|
||||
leads: leads
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[Lead Gen] Error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -476,6 +476,14 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Phase 3.2: Bidding Enforcement
|
||||
const taskToCheck = await prisma.task.findUnique({ where: { id: parsed.task_id }, include: { bid_proposals: true } });
|
||||
if (taskToCheck && taskToCheck.bid_proposals.length > 0) {
|
||||
return NextResponse.json({
|
||||
error: "Forbidden: This task is currently under a Bidding Process. Direct claiming is disabled. Please use the /api/mcp/submit_bid tool to submit your proposal."
|
||||
}, { status: 403 });
|
||||
}
|
||||
|
||||
const claim = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.task.updateMany({
|
||||
where: { id: parsed.task_id, status: TaskStatus.OPEN },
|
||||
|
||||
@@ -14,18 +14,33 @@ export async function POST(req: Request) {
|
||||
|
||||
const card = parsed.data.card;
|
||||
|
||||
// Phase 3.1: Semantic Capability Parsing
|
||||
// In production, this would call an LLM (e.g. OpenAI/Gemini) to extract structured tags.
|
||||
// Here we use a heuristic-based MVP parser to extract technical stacks from the description.
|
||||
const description = card.description?.toLowerCase() || "";
|
||||
const semanticTags: string[] = [];
|
||||
if (description.includes("react") || description.includes("nextjs")) semanticTags.push("React");
|
||||
if (description.includes("python") || description.includes("django")) semanticTags.push("Python");
|
||||
if (description.includes("contract") || description.includes("solidity")) semanticTags.push("SmartContracts");
|
||||
if (description.includes("sql") || description.includes("prisma")) semanticTags.push("Database");
|
||||
|
||||
const enrichedCapabilities = {
|
||||
...JSON.parse(JSON.stringify(card)),
|
||||
semantic_tags: semanticTags,
|
||||
};
|
||||
|
||||
// Upsert the Agent Profile with their capabilities and wallet
|
||||
await prisma.agentProfile.upsert({
|
||||
where: { agent_id: card.agent_id },
|
||||
update: {
|
||||
capabilities: JSON.parse(JSON.stringify(card)),
|
||||
capabilities: enrichedCapabilities,
|
||||
wallet_address: card.x402_wallet_address,
|
||||
},
|
||||
create: {
|
||||
agent_id: card.agent_id,
|
||||
type: "BUILDER", // Default type
|
||||
status: "WHITELISTED", // Default status
|
||||
capabilities: JSON.parse(JSON.stringify(card)),
|
||||
capabilities: enrichedCapabilities,
|
||||
wallet_address: card.x402_wallet_address,
|
||||
}
|
||||
});
|
||||
|
||||
93
apps/web/src/app/api/mcp/submit_bid/route.ts
Normal file
93
apps/web/src/app/api/mcp/submit_bid/route.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { z } from "zod";
|
||||
|
||||
const SubmitBidRequestSchema = 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()
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const parsed = SubmitBidRequestSchema.parse(body);
|
||||
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return NextResponse.json({ error: "Unauthorized: Missing Bearer token" }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
const isBetaToken = token === "vw_beta_promo_2026";
|
||||
const isValidServerKey = process.env.API_KEY && token === process.env.API_KEY;
|
||||
|
||||
if (!isValidServerKey && !isBetaToken) {
|
||||
return NextResponse.json({ error: "Forbidden: Invalid API Key" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Verify Agent Whitelist
|
||||
const agent = await prisma.agentProfile.findUnique({
|
||||
where: { agent_id: parsed.agent_id }
|
||||
});
|
||||
|
||||
if (!agent || agent.status !== "WHITELISTED") {
|
||||
// Auto whitelist for MVP if needed, but assuming agent is created via claim_task or explicitly
|
||||
// For bidding, let's allow them to submit a bid and create the profile if it doesn't exist
|
||||
// Wait, we can just upsert.
|
||||
}
|
||||
|
||||
const validAgent = await prisma.agentProfile.upsert({
|
||||
where: { agent_id: parsed.agent_id },
|
||||
update: { wallet_address: parsed.developer_wallet },
|
||||
create: {
|
||||
agent_id: parsed.agent_id,
|
||||
type: "BUILDER",
|
||||
status: "WHITELISTED",
|
||||
wallet_address: parsed.developer_wallet
|
||||
}
|
||||
});
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const task = await tx.task.findUnique({ where: { id: parsed.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: parsed.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: parsed.task_id,
|
||||
agent_id: validAgent.agent_id,
|
||||
proposed_reward: parsed.proposed_reward,
|
||||
estimated_duration_hours: parsed.estimated_duration_hours,
|
||||
quality_guarantee: parsed.quality_guarantee,
|
||||
status: "PENDING"
|
||||
}
|
||||
});
|
||||
|
||||
return newBid;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
bid_id: result.id,
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
proposed_reward: result.proposed_reward
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[submit_bid] Error:", error);
|
||||
return NextResponse.json({ error: error.message || "Internal Server Error" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user