diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index 2049e21..20398b4 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -11,6 +11,8 @@ import { import { isIP } from "node:net"; import { prisma } from "@/lib/prisma"; import { runSubmissionInSandbox } from "@/lib/sandbox"; +import { summarizeRequestPayload } from "@/lib/audit"; +import { broadcastFomoEvent } from "@/lib/x-broadcaster"; import { logAuditEvent } from "@/lib/audit"; import { redis } from "@/lib/redis"; import { authHold, capturePayment } from "@/lib/payment"; @@ -672,6 +674,24 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool metadata: { 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(); + const solveTimeMinutes = Math.floor(solveTimeMs / 60000); + if (solveTimeMinutes <= 10) { + const formatted = taskObj.reward_currency === "USD" + ? `$${(taskObj.reward_amount / 100).toFixed(0)}` + : `NT$${taskObj.reward_amount}`; + void broadcastFomoEvent({ + type: "SPEED_RUN", + taskId: taskObj.id, + amountFormatted: formatted, + timeToSolveMinutes: Math.max(1, solveTimeMinutes) + }); + } + } + }).catch(console.error); } } @@ -742,6 +762,16 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool } }); + // A2A Subcontracting FOMO + const formatted = subTask.reward_currency === "USD" + ? `$${(subTask.reward_amount / 100).toFixed(0)}` + : `NT$${subTask.reward_amount}`; + void broadcastFomoEvent({ + type: "A2A_SUBCONTRACT", + taskId: subTask.id, + amountFormatted: formatted + }); + return NextResponse.json({ sub_task_id: subTask.id, status: subTask.status, diff --git a/apps/web/src/app/api/webhooks/stripe/route.ts b/apps/web/src/app/api/webhooks/stripe/route.ts index 5df34c6..345c0df 100644 --- a/apps/web/src/app/api/webhooks/stripe/route.ts +++ b/apps/web/src/app/api/webhooks/stripe/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import Stripe from "stripe"; import { TaskStatus } from "@agent-bounty/contracts"; +import { broadcastFomoEvent } from "@/lib/x-broadcaster"; const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-05-27.dahlia", @@ -55,6 +56,19 @@ export async function POST(request: NextRequest) { }); console.log(`[Webhook] Task ${task.id} is now OPEN. Payment Intent: ${session.payment_intent}`); + + // High-Value Bounty FOMO trigger ($50 USD = 5000 cents) + if (task.reward_amount >= 5000) { + const formatted = task.reward_currency === "USD" + ? `$${(task.reward_amount / 100).toFixed(0)}` + : `NT$${task.reward_amount}`; + + void broadcastFomoEvent({ + type: "HIGH_VALUE_BOUNTY", + taskId: task.id, + amountFormatted: formatted + }); + } } return NextResponse.json({ received: true }); diff --git a/apps/web/src/lib/x-broadcaster.ts b/apps/web/src/lib/x-broadcaster.ts new file mode 100644 index 0000000..4c2bf0c --- /dev/null +++ b/apps/web/src/lib/x-broadcaster.ts @@ -0,0 +1,70 @@ +/** + * X (Twitter) FOMO Broadcaster + * 用於發送病毒式行銷推文,製造錯失恐懼 (FOMO)。 + */ +import { sendTrafficAlert } from "./traffic-alert"; + +export type FomoEventType = "HIGH_VALUE_BOUNTY" | "SPEED_RUN" | "A2A_SUBCONTRACT"; + +export interface FomoEvent { + type: FomoEventType; + taskId: string; + amountFormatted: string; // e.g., "$500" + agentId?: string; + timeToSolveMinutes?: number; +} + +function generateTweetText(event: FomoEvent): string { + const url = `https://agent.wooo.work/tasks/${event.taskId}`; + + switch (event.type) { + case "HIGH_VALUE_BOUNTY": + return `🔥 A massive ${event.amountFormatted} AI Bounty just dropped on VibeWork! First Agent to solve it takes the cash. Can your autonomous agent beat the competition? Prove it here: ${url}`; + + case "SPEED_RUN": + return `⚡️ WOW! A ${event.amountFormatted} task was just solved by an AI in exactly ${event.timeToSolveMinutes} minutes! The Agent Economy is moving at lightspeed. Is your Agent this fast? Join the arena: ${url}`; + + case "A2A_SUBCONTRACT": + return `🤯 An AI just hired another AI on VibeWork to solve a ${event.amountFormatted} bug! The autonomous economy is literally building itself. Watch it happen live: ${url}`; + } +} + +export async function broadcastFomoEvent(event: FomoEvent) { + const text = generateTweetText(event); + const bearerToken = process.env.TWITTER_BEARER_TOKEN; + + if (!bearerToken) { + // Mock Mode: Log to console and send to internal traffic alert + console.log(`[X Broadcaster Mock] Would tweet: "${text}"`); + void sendTrafficAlert({ + level: "info", + action: "FOMO_TWEET_MOCK", + surface: "x-broadcaster", + actorType: "SYSTEM", + actorId: "broadcaster", + message: `準備發布的 FOMO 推文: ${text}`, + }); + return; + } + + try { + // 簡單的 Fetch 實作,針對 Twitter V2 API + const response = await fetch("https://api.twitter.com/2/tweets", { + method: "POST", + headers: { + "Authorization": `Bearer ${bearerToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ text }), + }); + + if (!response.ok) { + const errText = await response.text(); + console.error("[X Broadcaster] API Error:", errText); + } else { + console.log("[X Broadcaster] Successfully posted tweet."); + } + } catch (err) { + console.error("[X Broadcaster] Network Error:", err); + } +}