Integration guide · Next.js

How to stop fake signups in Next.js

Two checks, about ten minutes: a script tag that fixes typos and stops throwaway emails in the browser, and a Server Action check that bots cannot skip.

By the Vetto team · Updated

Quick answer

Load the Vetto script in your root layout, then check the email again in the Server Action before you create the user. The full, tested code is in the nextjs-vetto-example repo (MIT): clone it, npm install, npm run dev.

1. Add the script to the layout

It finds email fields by itself, checks them on blur and on submit, and shows the message under the field. Your form component does not change. Get the pk_… key from a free account; it also works on localhost.

app/layout.tsx
// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://usevetto.com/v.js"
          data-site={process.env.NEXT_PUBLIC_VETTO_SITE_KEY}
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

2. Check again in the Server Action

A bot can post to your Server Action without ever loading the page. The action reads the email and the vetto_token field the script added, and asks for a decision.

app/actions.ts
// app/actions.ts
'use server';
import { vettoDecision } from '@/lib/vetto';

export async function signup(_prev: unknown, formData: FormData) {
  const email = String(formData.get('email') ?? '').trim();
  // Added by the script on submit: a signed verdict on Pro, empty on Free.
  const token = String(formData.get('vetto_token') ?? '') || null;

  const decision = await vettoDecision(email, token);
  if (!decision.ok) return { status: 'error', message: decision.message };

  // Create the user here.
  return { status: 'ok', email };
}
lib/vetto.ts
// lib/vetto.ts (Free version: keyless domain lookup, fails open)
import 'server-only';

const DEAD = new Set(['nxdomain', 'no_mx_a', 'null_mx', 'no_mail']);

export async function vettoDecision(email: string, _token: string | null) {
  const domain = email.split('@').pop()?.toLowerCase();
  if (!domain) return { ok: true } as const;
  try {
    const res = await fetch('https://usevetto.com/v1/audit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ domains: [domain] }),
      signal: AbortSignal.timeout(2500),
      cache: 'no-store',
    });
    const r = (await res.json()).results?.[domain];
    if (r?.disposable) return { ok: false, message: 'Please use a permanent email address.' } as const;
    if (r && DEAD.has(r.status)) return { ok: false, message: 'This email domain cannot receive mail.' } as const;
  } catch {}
  return { ok: true } as const;
}

On Pro, vettoDecision verifies the signed token offline instead (HMAC, 15 minutes, bound to the email) and calls /v1/check with your secret key when the token is missing. Both versions are in the example repo; the API is documented in the docs.

What each layer catches

AddressBrowser scriptServer Action
jane@mailinator.comBlocked with a messageRejected (throwaway)
jane@gmial.com"Did you mean jane@gmail.com?" with a one-click fixRejected
jane@doesnotexist-xyz-123.comBlocked (cannot receive mail)Rejected (dead domain)
jane@privaterelay.appleid.comAllowed (Apple relay)Allowed
Bot posting to the action directlyNever runsRejected if the domain is bad

Using an auth library? Auth.js / NextAuth · Supabase · Better Auth plugin · free trial abuse.

Frequently asked questions

How do I stop fake signups in Next.js?

Check the email twice. In the browser, to fix typos and stop throwaway inboxes before submit. Then in the Server Action or route handler that creates the user, because bots post straight to your endpoint and skip the browser. Fail open in both places so an outage never blocks real users.

Does the Vetto script work with Server Actions and React forms?

Yes. It listens for submit in the capture phase on window, runs the check, then re-submits with requestSubmit(), so React and <form action={...}> handle the submit as usual. It adds a hidden vetto_token field that your Server Action can read.

Can I do this without a paid plan?

Yes. The browser script is free on one site (1,000 emails a month, no card), and the server check in this guide uses the free, keyless /v1/audit lookup (300 calls a day per server IP). Pro ($12/mo) adds signed tokens, the full server API, Gmail alias detection and VPN signals.

Should I block Apple Hide My Email and other relays?

No. Relay addresses forward to a real, long-lived inbox, and blocking them turns away privacy-minded customers. Vetto lets them through by default.

Give your signup form sharp eyes.

Free on one site. One line of HTML. Remove it any time and your form works exactly as before.

Founding offer: Pro for life for $69 once, first 100 buyers, until Nov 27.