SPB Git

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%
2.4 KB · 79 lines typescript
Raw Blame History
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