feat(Phase1): Implement Scout API, Stripe Webhooks, and Builder Whitelisting
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:
@@ -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(),
|
||||
|
||||
106
apps/web/src/app/api/scout/draft/route.ts
Normal file
106
apps/web/src/app/api/scout/draft/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
62
apps/web/src/app/api/webhooks/stripe/route.ts
Normal file
62
apps/web/src/app/api/webhooks/stripe/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user