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.