Next.js (App Router)

This recipe checks signups in an App Router route handler. It works the same with any auth library that lets you run code before creating the user.

1. A small client with fail-open

// lib/burnguard.ts
export type BurnGuardDecision = "allow" | "limit" | "review" | "block";

export interface BurnGuardResult {
  decision: BurnGuardDecision;
  recommended_credit_fraction?: number;
  reasons?: { code: string; weight: number; detail: string }[];
  id?: string;
}

export async function checkSignup(input: {
  email: string;
  ip?: string;
  deviceToken?: string;
  userAgent?: string;
  externalId?: string;
}): Promise<BurnGuardResult> {
  try {
    const res = await fetch(`${process.env.BURNGUARD_URL}/v1/check`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.BURNGUARD_SECRET_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event: "signup",
        email: input.email,
        ip: input.ip,
        device_token: input.deviceToken,
        user_agent: input.userAgent,
        external_id: input.externalId,
      }),
      signal: AbortSignal.timeout(1500),
      cache: "no-store",
    });
    if (!res.ok) return { decision: "allow" };
    return (await res.json()) as BurnGuardResult;
  } catch {
    return { decision: "allow" }; // never block signups because of us
  }
}

2. The signup route

// app/api/signup/route.ts
import { NextResponse } from "next/server";
import { checkSignup } from "@/lib/burnguard";

const FREE_CREDITS = 500;

export async function POST(req: Request) {
  const { email, password, deviceToken } = await req.json();
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();

  const check = await checkSignup({
    email,
    ip,
    deviceToken,
    userAgent: req.headers.get("user-agent") ?? undefined,
  });

  if (check.decision === "block") {
    return NextResponse.json({ error: "We couldn't create this account." }, { status: 403 });
  }

  const credits =
    check.decision === "review" ? 0 : Math.floor(FREE_CREDITS * (check.recommended_credit_fraction ?? 1));

  const user = await createUser({ email, password, credits, needsVerification: check.decision === "review" });
  return NextResponse.json({ id: user.id });
}

Only trust x-forwarded-for when your host (Vercel, a load balancer, nginx) sets it.

3. The device token on the client

"use client";
import Script from "next/script";

export function BurnGuardScript() {
  return <Script src={`${process.env.NEXT_PUBLIC_BURNGUARD_URL}/sdk.js`} data-key={process.env.NEXT_PUBLIC_BURNGUARD_PK} strategy="afterInteractive" />;
}

// In your signup handler:
const deviceToken = await (window as any).BurnGuard?.token().catch(() => undefined);

See Decisions and credit gating for what to do with each decision.