Engineering manualAPI route source
/api/intake-peek
Exact current route handler: hosted-app/app/api/intake-peek/route.js
Methods: POST. Input signals: JSON body. Literal response-status signals: 401, 429.
Source: hosted-app/app/api/intake-peek/route.js. Imports: ../../../lib/all-rest/alpha-parking.mjs, ../../../lib/comprehensive-pull.mjs, ../../../lib/local/intake-peek.mjs, ../../../lib/supabase-rest.mjs, next/server. Exact source is shown for inspection; its presence does not prove authentication, validation or live behavior.
All API routes · API and execution contract
// POST /api/intake-peek — the vendor's one look at a brand before the interview.
//
// The PAID sibling of /api/interview-prefill, on the same second-wave terms:
// the confirm screen opens at $0.00 on the instant card's deterministic
// prefill, and suggestion chips arrive afterwards from whoever has something
// to offer. That route asks a model to READ the homepage; this one asks
// context.dev what it independently KNOWS about the domain — industry
// classification, registered address, title — 10 vendor credits per call,
// billed to us, never to the customer, and never able to change an answer
// without a human tapping the chip.
//
// The interview-prefill route's charter ("IT FETCHES NOTHING") is why this
// is a separate endpoint rather than a flag on that one: the property that
// makes the confirm screen instant survives only if every network-touching
// suggestion is its own explicitly-gated second wave.
//
// BOUNDED THE SAME THREE WAYS:
// - SIGNED IN ONLY (401 for anonymous).
// - ONE BUCKET PER USER PER HOUR — stricter than prefill's (each call
// spends real vendor credits).
// - VENDOR OFF -> {fields: []}, byte-identical dialog, no error chrome.
import { NextResponse } from "next/server";
import { bearerTokenFromHeader, resolveAuthContext } from "../../../lib/comprehensive-pull.mjs";
import { applyAlphaParkingToEnv } from "../../../lib/all-rest/alpha-parking.mjs";
import { supabaseConfigFromEnv } from "../../../lib/supabase-rest.mjs";
import { INTAKE_PEEK_SCHEMA_VERSION, runIntakePeek } from "../../../lib/local/intake-peek.mjs";
export const runtime = "nodejs";
export const maxDuration = 30;
const PEEKS_PER_HOUR = 12;
const WINDOW_MS = 60 * 60 * 1000;
const buckets = new Map();
const allowRequest = (key) => {
const now = Date.now();
const bucket = buckets.get(key);
if (!bucket || now - bucket.startedAt >= WINDOW_MS) {
buckets.set(key, { startedAt: now, used: 1 });
if (buckets.size > 5000) buckets.clear();
return true;
}
if (bucket.used >= PEEKS_PER_HOUR) return false;
bucket.used += 1;
return true;
};
/** Exported for the test: the bucket is per-user and resets on the hour. */
export const __resetIntakePeekBuckets = () => buckets.clear();
export async function POST(request) {
const startedMs = Date.now();
const { supabaseUrl, supabaseAnonKey } = supabaseConfigFromEnv(process.env);
const auth = await resolveAuthContext({
token: bearerTokenFromHeader(request.headers.get("authorization")),
supabaseUrl,
supabaseAnonKey
});
if (!auth.authenticated) {
return NextResponse.json(
{ status: "refused", reason: "signup_required", detail: auth.reason || "authentication required" },
{ status: 401 }
);
}
if (!allowRequest(auth.user.id)) {
return NextResponse.json(
{ status: "rate_limited", reason: "too_many_peeks", detail: `at most ${PEEKS_PER_HOUR} vendor peeks per hour` },
{ status: 429 }
);
}
const body = await request.json().catch(() => null);
const domain = String(body?.domain || body?.url || "").slice(0, 500);
const result = await runIntakePeek({ domain, env: applyAlphaParkingToEnv(process.env) });
// One shape out, whatever happened — the dialog draws chips for fields and
// nothing for an empty list, exactly like the AI route.
return NextResponse.json({
status: "ok",
schema_version: INTAKE_PEEK_SCHEMA_VERSION,
peek_status: result.status,
...(result.reason ? { reason: result.reason } : {}),
fields: result.fields,
cost: result.cost,
elapsed_ms: Date.now() - startedMs
});
}