feat(Phase1): Implement Scout API, Stripe Webhooks, and Builder Whitelisting
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-07 13:58:13 +08:00
parent aacf45d87d
commit 8003aceab0
32 changed files with 8280 additions and 305 deletions

View File

@@ -65,11 +65,19 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
case "claim_task": {
const parsed = ClaimTaskRequestSchema.parse(body);
// Verify Agent Whitelist
const agent = await prisma.agentProfile.findUnique({
where: { agent_id: parsed.agent_id }
});
if (!agent || agent.status !== "WHITELISTED") {
return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 });
}
const claim = await prisma.$transaction(async (tx) => {
const updated = await tx.task.updateMany({
where: { id: parsed.task_id, status: TaskStatus.OPEN },
data: { status: TaskStatus.EXECUTING }
data: { status: TaskStatus.EXECUTING, builder_id: agent.agent_id }
});
if (updated.count === 0) {
@@ -81,6 +89,7 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
const newClaim = await tx.claim.create({
data: {
task_id: task.id,
agent_id: agent.agent_id,
developer_wallet: parsed.developer_wallet,
status: TaskStatus.EXECUTING,
claim_token: crypto.randomUUID(),

View File

@@ -0,0 +1,106 @@
import { NextRequest, NextResponse } from "next/server";
import { ScoutDraftRequestSchema, ScoutDraftResponseSchema, TaskStatus } from "@agent-bounty/contracts";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2026-05-27.dahlia",
});
export async function POST(request: NextRequest) {
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];
if (process.env.API_KEY && token !== process.env.API_KEY) {
return NextResponse.json({ error: "Forbidden: Invalid API Key" }, { status: 403 });
}
try {
const body = await request.json();
const parsed = ScoutDraftRequestSchema.parse(body);
// Validate scout_id exists and is whitelisted
const scout = await prisma.agentProfile.findUnique({
where: { agent_id: parsed.scout_id }
});
if (!scout || scout.status !== "WHITELISTED") {
return NextResponse.json({ error: "Forbidden: Scout Agent is not whitelisted" }, { status: 403 });
}
// Create DRAFT task
const task = await prisma.task.create({
data: {
title: parsed.title,
description: parsed.description,
status: TaskStatus.DRAFT,
difficulty: "COMPONENT", // Defaulting for Phase 1
scope_clarity_score: 1.0,
reward_amount: parsed.reward_amount,
reward_currency: parsed.reward_currency,
required_stack: parsed.required_stack,
scout_id: scout.agent_id,
acceptance_criteria: {
validation_mode: "VITEST_UNIT",
test_file_content: parsed.test_file_content,
}
}
});
// Create Stripe Checkout Session
// We do a manual capture session so the funds are only captured when Judge passes
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
mode: "payment",
line_items: [
{
price_data: {
currency: parsed.reward_currency.toLowerCase(),
product_data: {
name: `VibeWork Task: ${parsed.title}`,
description: "Auth-Hold. Funds will only be captured when task is judged PASS.",
},
unit_amount: parsed.reward_amount,
},
quantity: 1,
},
],
payment_intent_data: {
capture_method: "manual",
metadata: {
task_id: task.id,
scout_id: scout.agent_id,
}
},
// You should set these to actual frontend URLs
success_url: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/tasks/${task.id}?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/tasks/create`,
});
// Save session ID so webhook can find it
await prisma.task.update({
where: { id: task.id },
data: { stripe_checkout_session_id: session.id }
});
const responseData = {
task_id: task.id,
checkout_url: session.url!,
status: TaskStatus.DRAFT,
};
ScoutDraftResponseSchema.parse(responseData); // strict output validation
return NextResponse.json(responseData);
} catch (error: any) {
console.error("[Scout API Error]", error);
if (error.name === "ZodError") {
return NextResponse.json({ error_type: "InvalidParams", message: error.errors }, { status: 400 });
}
return NextResponse.json({ error_type: "InternalError", message: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
import { TaskStatus } from "@agent-bounty/contracts";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2026-05-27.dahlia",
});
export async function POST(request: NextRequest) {
const payload = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature || !process.env.STRIPE_WEBHOOK_SECRET) {
return NextResponse.json({ error: "Missing signature or webhook secret" }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err: any) {
console.error(`[Webhook Error]`, err.message);
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
try {
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const task = await prisma.task.findFirst({
where: { stripe_checkout_session_id: session.id }
});
if (!task) {
console.error(`[Webhook] Task not found for session: ${session.id}`);
return NextResponse.json({ received: true });
}
// Payment is authorized (Auth Hold)
// Save the payment_intent_id and set status to OPEN
await prisma.task.update({
where: { id: task.id },
data: {
stripe_payment_intent_id: session.payment_intent as string,
status: TaskStatus.OPEN
}
});
console.log(`[Webhook] Task ${task.id} is now OPEN. Payment Intent: ${session.payment_intent}`);
}
return NextResponse.json({ received: true });
} catch (error: any) {
console.error("[Webhook Processing Error]", error);
return NextResponse.json({ error: "Internal Error" }, { status: 500 });
}
}