Clerk

If you use Clerk, first turn on Clerk Protect for disposable-email and subaddress blocking (Clerk docs). BurnGuard adds what Clerk doesn't do for you: credit gating, device reuse across your own signups, clustering and a dollars-saved number.

Clerk's user.created webhook doesn't include the end user's IP or a device token, so the most accurate place to check is the user's first authenticated request, before you grant free credits.

// app/api/onboard/route.ts (called once from your app after sign-up)
import { auth, clerkClient } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { checkSignup } from "@/lib/burnguard"; // see the Next.js recipe

const FREE_CREDITS = 500;

export async function POST(req: Request) {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const client = await clerkClient();
  const user = await client.users.getUser(userId);
  if (user.publicMetadata.credits !== undefined) return NextResponse.json({ ok: true }); // already onboarded

  const { deviceToken } = await req.json();
  const check = await checkSignup({
    email: user.primaryEmailAddress?.emailAddress ?? "",
    ip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
    deviceToken,
    userAgent: req.headers.get("user-agent") ?? undefined,
    externalId: userId,
  });

  if (check.decision === "block") {
    await client.users.banUser(userId);
    return NextResponse.json({ error: "blocked" }, { status: 403 });
  }

  const fraction = check.decision === "review" ? 0 : check.recommended_credit_fraction ?? 1;
  await client.users.updateUserMetadata(userId, {
    publicMetadata: { credits: Math.floor(FREE_CREDITS * fraction), needsVerification: check.decision === "review" },
  });
  return NextResponse.json({ ok: true, decision: check.decision });
}

Pass the externalId so you can later report usage with /v1/usage and see the dollars burned by flagged accounts.