spb/worthdoing Public
Autonomous investigation agent that discovers, challenges, and ranks things genuinely worth doing — Claude + Firecrawl, Next.js 16, PostgreSQL
TypeScript 91.5%
SQL 5.8%
CSS 2.2%
1/**2 * WorthDoing.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: src/app/api/investigations/route.ts6 * Description: Create + start investigations (POST) and list them (GET), with basic rate limiting.7 */8import { NextRequest, NextResponse } from "next/server";9import { desc } from "drizzle-orm";10import { z } from "zod";11import { db } from "@/lib/db/client";12import { investigations } from "@/lib/db/schema";13import { createAndStartInvestigation } from "@/lib/agent/runner";1415export const dynamic = "force-dynamic";1617const createSchema = z.object({18 objective: z.string().min(8).max(500),19});2021// Simple in-memory rate limit: max 4 new investigations per 10 minutes per IP.22const globalForRl = globalThis as unknown as { __wdRate?: Map<string, number[]> };23const rateMap = (globalForRl.__wdRate ??= new Map<string, number[]>());24const RL_WINDOW_MS = 10 * 60 * 1000;25const RL_MAX = 4;2627export async function POST(req: NextRequest) {28 const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "local";29 const now = Date.now();30 const hits = (rateMap.get(ip) ?? []).filter((t) => now - t < RL_WINDOW_MS);31 if (hits.length >= RL_MAX) {32 return NextResponse.json(33 { error: "Rate limit exceeded — max 4 investigations per 10 minutes." },34 { status: 429 },35 );36 }3738 let body: unknown;39 try {40 body = await req.json();41 } catch {42 return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });43 }44 const parsed = createSchema.safeParse(body);45 if (!parsed.success) {46 return NextResponse.json(47 { error: "objective must be a string of 8–500 characters." },48 { status: 400 },49 );50 }5152 hits.push(now);53 rateMap.set(ip, hits);5455 const inv = await createAndStartInvestigation(parsed.data.objective);56 return NextResponse.json(57 { id: inv.id, objective: inv.objective, status: inv.status, createdAt: inv.createdAt },58 { status: 201 },59 );60}6162export async function GET() {63 const rows = await db64 .select({65 id: investigations.id,66 objective: investigations.objective,67 status: investigations.status,68 phase: investigations.phase,69 outcome: investigations.outcome,70 budgetUsed: investigations.budgetUsed,71 createdAt: investigations.createdAt,72 completedAt: investigations.completedAt,73 })74 .from(investigations)75 .orderBy(desc(investigations.createdAt))76 .limit(50);77 return NextResponse.json({ investigations: rows });78}79