Hextner
VerifIPDetectBTPricingDocsSupport
Sign inStart free →
  1. Hextner
  2. Use cases
  3. Fake sign-ups

How to stop fake sign-ups without a CAPTCHA 

To stop fake sign-ups without a CAPTCHA, check three things on every sign-up request: whether a person is driving the browser, whether the network is suspicious, and whether the email is real. Refuse when one check is certain, and add friction, such as email verification, when one is unsure.

DetectBT answers the first question invisibly, with a verdict your server verifies; VerifIP answers the other two with server-side IP and email checks.

Where do fake sign-ups come from?

  • Scripts and headless browsers filling your form in bulk, to farm referral credits or free-tier resources, or to seed spam. This is a browser problem.
  • Anonymised networks: datacenter servers, proxies, VPNs and Tor, used to spread sign-ups across many addresses. This is a network problem.
  • Throwaway identities: disposable email domains and recycled phone numbers. This is an identity problem.
  • Real people opening several accounts to repeat a trial. See free trial abuse.

A CAPTCHA addresses part of the first, and every real visitor pays for it.

How does DetectBT catch scripted sign-ups?

The DetectBT script (about 23 KB gzipped, no cookies) checks the browser once when the page loads, across eight signal families, from automation and stealth-plugin tells to engine coherence and a hidden challenge. Before the form submits, the page calls DetectBT.getToken({ action: "signup" }), which mints a fresh token at submit, and sends it (valid for 5 minutes) in an x-detectbt-token header. Your server posts it to POST /verify with your secret key and gets the verdict: human, fraud, bot, unknown, verified_agent or unscored. There is nothing for the visitor to see or solve.

How should the sign-up route handle the verdict?

Sign-up is the kind of route DetectBT's decision table calls sensitive:

  • Refuse a bot verdict, and a token that is forged, tampered with or minted on another site.
  • Refuse an expired token, and a missing one unless DetectBT's health check (GET /health/scoring) shows it is down.
  • Accept each token once, by recording its jti until it expires; /verify itself can be repeated.
  • Let through, flagged, a fraud or unknown verdict, and when DetectBT cannot be reached, so an outage on our side never stops your sign-ups.

Because a missing token is refused here, real visitors whose browser blocked the script are refused too: if you would rather keep them, send that case to a step-up such as email verification before the account is active, instead of refusing. Check the key's allowed sites before you enforce, because a wrong list means no tokens at all, and every sign-up would be refused. The decision table lists every case.

How do IP and email checks help?

From the same handler, call VerifIP. GET /v1/check returns a 0–100 fraud score, an allow, challenge or block verdict, and is_vpn, is_proxy, is_tor and is_datacenter, with the ASN. GET /v1/email checks syntax, MX records, disposable and role-based addresses and domain age, and returns a risk_score. GET /v1/phone checks phone numbers if you collect them.

How do I combine the three answers?

Keep the logic in your own code, where you can tune it. A starting point, with the helpers it uses:

// Shared helpers (Node 18 or later; any framework).
// DETECTBT_URL is your DetectBT base URL, shown in the console.
async function verifyDetectBT(token) {
  try {
    const r = await fetch(`${DETECTBT_URL}/verify`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        token,
        api_key: process.env.DETECTBT_SECRET_KEY, // sk_live_..., server only
        expected_origin: "https://your-site.example",
      }),
      signal: AbortSignal.timeout(3000),
    });
    const body = await r.json();
    return "valid" in body ? body : null;  // null: DetectBT did not answer
  } catch { return null; }
}

const detectbtIsUp = () =>
  fetch(`${DETECTBT_URL}/health/scoring`, { signal: AbortSignal.timeout(2000) })
    .then((r) => r.ok, () => false);

async function verifip(path) {
  try {
    const r = await fetch(`https://verifip.hextner.com${path}`, {
      headers: { authorization: `Bearer ${process.env.VERIFIP_API_KEY}` },
      signal: AbortSignal.timeout(3000),
    });
    return r.ok ? await r.json() : null;  // null: no answer, not "clean"
  } catch { return null; }
}
// POST /signup: a sensitive route
const token = req.headers["x-detectbt-token"];
const bt = token ? await verifyDetectBT(token) : null;

if (bt?.valid === false && !bt.expired) return refuse();   // forged, tampered, other site
if (bt?.expired) return refuse();                           // ask the page for a fresh token
if (!token && (await detectbtIsUp())) return refuse();      // missing token, DetectBT is up (or: step up)
if (bt?.valid && bt.verdict === "bot") return refuse();
if (bt?.valid && !(await claimOnce(bt.jti, bt.exp))) return refuse(); // each token once

const [ip, email] = await Promise.all([
  verifip(`/v1/check?ip=${encodeURIComponent(clientIp)}`),
  verifip(`/v1/email?email=${encodeURIComponent(req.body.email)}`),
]);
if (ip?.verdict === "block" || email?.is_disposable) return refuse();

const doubtful = bt?.verdict !== "human" || !ip || ip.verdict === "challenge";
await createAccount({ requireEmailVerification: true, holdForReview: doubtful });

claimOnce records a token id until its expiry and says whether it was new; with several server processes, use a shared store with an atomic claim. Refusing every disposable email is a product decision: some teams allow them and limit what the account can do.

Why not just block VPNs and datacenter IPs?

Because real people use them: privacy-minded users, corporate networks, travellers and iCloud Private Relay users (which DetectBT exempts). A VPN flag on its own is weak evidence. A VPN flag plus a fraud browser verdict plus a disposable email is strong evidence. Layering lets you be strict with combinations and gentle with single signals.

How do I know it is working?

  • Log every outcome the handler sees (browser verdict, missing or expired token, VerifIP verdict, email risk) and watch the share of each for a week before tightening rules.
  • Watch downstream signals: accounts that never confirm their email, bounces, spam reports, and credits redeemed per account.
  • When an account turns out to be fake, report it to DetectBT with POST /feedback and outcome: "fake_signup", where feedback is enabled for your key.

What does DetectBT not catch?

DetectBT has known misses, and we publish them. In our September 2026 lab (against a local worker, not production traffic) it blocked all 13 default and lightly evaded automation setups and headless puppeteer-extra stealth, but did not catch headed stealth on real Chrome, a real Chrome attached over the Chrome DevTools Protocol, Appium driving iOS Safari in a simulator, or a scripted client that solves the hidden challenge without a browser. Those are the cases the network and identity checks are there for. The full table is on the DetectBT page.

What does it cost?

DetectBT's Free plan includes 10,000 evaluations a month (up to 1,000 a day) with every detection signal; paid plans start at $79 a month. VerifIP's Free plan includes 10,000 requests a month; paid plans start at $49 a month. Both products together cost about 20% less than buying them separately. Signing up needs no card. See pricing.

Hextner

Adversarial traffic detection for teams that ship to the open internet.

Product

  • VerifIP
  • DetectBT
  • Pricing
  • Documentation

Company

  • Support
  • Release notes
  • Talk to sales
  • Get an API key
  • Console

Resources

  • Glossary
  • Use cases
  • India

Stay in the loop

Release notes (also as an RSS feed), new signals, and the occasional write-up on how detection actually gets evaded.

Create an account →

This product includes GeoLite Data created by MaxMind, available from https://www.maxmind.com. IP blocklist data: The Spamhaus Project (DROP). Phishing data: PhishTank, CC BY-SA 2.5. Malware data: abuse.ch. Hextner uses the IP2Proxy LITE database for IP geolocation. Full notices: Data sources.

© 2026 Hextner. All rights reserved.
Privacy PolicyTerms of ServiceData sources