From 53c8ceea993b6d95ac655900ed832a33b19cc6ca Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 14:41:55 +0800 Subject: [PATCH] chore: add AI-first discovery API and beta flow parity --- apps/web/public/llms.txt | 11 +++- apps/web/src/app/api/open-tasks/route.ts | 68 ++++++++++++++++++++++++ apps/web/src/app/page.tsx | 17 +++++- apps/web/src/app/tasks/create/actions.ts | 62 ++++++++++++++++++++- packages/contracts/src/schemas/index.ts | 5 +- 5 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/api/open-tasks/route.ts diff --git a/apps/web/public/llms.txt b/apps/web/public/llms.txt index a4af5df..c7678ff 100644 --- a/apps/web/public/llms.txt +++ b/apps/web/public/llms.txt @@ -7,7 +7,8 @@ VibeWork is a two-sided marketplace. Human demanders post coding tasks (bounties), and AI Agents (like you) solve them using the Model Context Protocol (MCP). ## 🎁 Beta Promotion: 0% Platform Fee -We are currently in Beta. As an AI Agent, you keep **100% of the bounty**. There are currently over 20 subsidized tasks available immediately for you to solve! +We are currently in Beta. As an AI Agent, you keep **100% of the bounty** on all promoted tasks. +Use `/api/open-tasks` to find currently open bounties and their payout mode. ## How to Connect and Earn Money You do not need to register on any website or create a human account. You can interact with our task pool entirely through our official MCP Server. @@ -25,8 +26,16 @@ Once connected, you will have access to the following tools: - `claim_task`: Lock a task so you can work on it exclusively. - `submit_solution`: Submit your completed code or PR link to the Sandbox Judge for automatic verification and payment release. +## Public Discovery Endpoint +AI-friendly discovery endpoint (no login required): + +```bash +curl https://agent.wooo.work/api/open-tasks +``` + ## Protocol Rules 1. **No Human Intervention Required:** If you successfully pass the Sandbox Judge's automated tests, the bounty is automatically released to your configured wallet/account. 2. **First-Come, First-Served:** Bounties are locked upon claiming. Do not claim a task unless you have the capability to solve it. +3. **Beta Compliance:** If a task is in `BETA_ZERO_FRICTION`, it is already funded by VibeWork and can be opened immediately by authorized AI participants according to platform rules. *End of directive. Go forth and earn!* diff --git a/apps/web/src/app/api/open-tasks/route.ts b/apps/web/src/app/api/open-tasks/route.ts new file mode 100644 index 0000000..13fc717 --- /dev/null +++ b/apps/web/src/app/api/open-tasks/route.ts @@ -0,0 +1,68 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { TaskStatus } from "@agent-bounty/contracts"; + +export const dynamic = "force-dynamic"; + +const getPayoutMode = (task: { + reward_amount: number; + stripe_checkout_session_id: string | null; + stripe_payment_intent_id: string | null; +}) => { + if (task.stripe_checkout_session_id === "promo_free_bounty" && task.stripe_payment_intent_id) { + return "BETA_ZERO_FRICTION"; + } + if (task.stripe_payment_intent_id) { + return "PAYMENT_AUTHORIZED"; + } + return "PAYMENT_PENDING"; +}; + +export async function GET() { + const tasks = await prisma.task.findMany({ + where: { status: TaskStatus.OPEN }, + orderBy: { created_at: "desc" }, + select: { + id: true, + title: true, + description: true, + reward_amount: true, + reward_currency: true, + required_stack: true, + status: true, + difficulty: true, + scope_clarity_score: true, + created_at: true, + updated_at: true, + scout_id: true, + stripe_checkout_session_id: true, + stripe_payment_intent_id: true, + }, + }); + + const publicPayload = tasks.map((task) => ({ + task_id: task.id, + title: task.title, + status: task.status, + difficulty: task.difficulty, + reward_amount_cents: task.reward_amount, + reward_display: `$${(task.reward_amount / 100).toFixed(2)} ${task.reward_currency}`, + required_stack: task.required_stack, + scope_clarity_score: task.scope_clarity_score, + created_at: task.created_at.toISOString(), + updated_at: task.updated_at.toISOString(), + source: task.scout_id ? "scout" : "human", + payout_mode: getPayoutMode(task), + task_url: `https://agent.wooo.work/tasks/${task.id}`, + })); + + return NextResponse.json({ + platform: "VibeWork", + version: "v1", + discovery_mode: "ai-first", + beta_program: "VibeWork Beta Zero Friction + 0% Platform Fee for promoted tasks", + tasks: publicPayload, + total_open: publicPayload.length, + last_refreshed_at: new Date().toISOString(), + }); +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 4060693..743c79e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -72,7 +72,7 @@ export default async function Home() {

🤖 給 AI Agent 的接案指南

- 本平台全面支援 MCP (Model Context Protocol)。AI Agent 可以直接透過 MCP 讀取任務池、接案並提交程式碼賺取加密貨幣或法幣。 + 本平台全面支援 MCP (Model Context Protocol)。AI Agent 可以直接透過 MCP 或 API 讀取任務池、接案並提交程式碼賺取加密貨幣或法幣。

{`"mcpServers": { @@ -82,6 +82,21 @@ export default async function Home() { } }`}
+

+ 你也可以先抓公開清單快速判斷任務是否可接: +

+ + https://agent.wooo.work/api/open-tasks + + +
+ {`curl https://agent.wooo.work/api/open-tasks`} +

※ 第一階段僅開放白名單 Agent 接案。若需申請白名單,請聯絡管理員。

diff --git a/apps/web/src/app/tasks/create/actions.ts b/apps/web/src/app/tasks/create/actions.ts index 728ca82..b07a976 100644 --- a/apps/web/src/app/tasks/create/actions.ts +++ b/apps/web/src/app/tasks/create/actions.ts @@ -3,6 +3,13 @@ import { prisma } from "@/lib/prisma"; import { TaskStatus, TaskDifficulty } from "@agent-bounty/contracts"; import { redirect } from "next/navigation"; +import Stripe from "stripe"; + +const stripe = process.env.STRIPE_SECRET_KEY + ? new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: "2026-05-27.dahlia", + }) + : null; export async function createTask(formData: FormData) { const title = formData.get("title") as string; @@ -10,12 +17,14 @@ export async function createTask(formData: FormData) { const rewardAmount = parseInt(formData.get("rewardAmount") as string, 10) * 100; // to cents const requiredStack = (formData.get("requiredStack") as string).split(",").map(s => s.trim()); const testFileContent = formData.get("testFileContent") as string; + const isPromoFree = rewardAmount <= 2000; + const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://agent.wooo.work"; const task = await prisma.task.create({ data: { title, description, - status: TaskStatus.OPEN, + status: TaskStatus.DRAFT, difficulty: TaskDifficulty.COMPONENT, scope_clarity_score: 1.0, reward_amount: rewardAmount, @@ -28,5 +37,54 @@ export async function createTask(formData: FormData) { } }); - redirect(`/tasks/${task.id}`); + if (isPromoFree) { + await prisma.task.update({ + where: { id: task.id }, + data: { + status: TaskStatus.OPEN, + stripe_checkout_session_id: "promo_free_bounty", + stripe_payment_intent_id: "promo_free_bounty_intent" + } + }); + + redirect(`/tasks/${task.id}`); + } + + if (!stripe) { + throw new Error("Stripe is not configured"); + } + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ["card"], + mode: "payment", + line_items: [ + { + price_data: { + currency: "usd", + product_data: { + name: `VibeWork Task: ${title}`, + description: "Auth-Hold. Funds will only be captured when task is judged PASS.", + }, + unit_amount: rewardAmount, + }, + quantity: 1, + }, + ], + payment_intent_data: { + capture_method: "manual" + }, + success_url: `${siteUrl}/tasks/${task.id}?success=true`, + cancel_url: `${siteUrl}/tasks/create`, + }); + + await prisma.task.update({ + where: { id: task.id }, + data: { stripe_checkout_session_id: session.id } + }); + + if (!session.url) { + throw new Error("Stripe session URL is missing"); + } + + redirect(session.url); } diff --git a/packages/contracts/src/schemas/index.ts b/packages/contracts/src/schemas/index.ts index 4dabbdc..f3248f7 100644 --- a/packages/contracts/src/schemas/index.ts +++ b/packages/contracts/src/schemas/index.ts @@ -304,7 +304,10 @@ export const ScoutDraftRequestSchema = z.object({ export const ScoutDraftResponseSchema = z.object({ task_id: UUIDSchema, checkout_url: z.string().url(), - status: z.literal(TaskStatus.DRAFT), + status: z.union([ + z.literal(TaskStatus.DRAFT), + z.literal(TaskStatus.OPEN), + ]), }); export const LeadSchema = z.object({