Enforce one account per device
Emails, phone numbers and proxies all rotate for cents. The device is the one signup input an attacker can't trivially rotate — provided it proves itself with hardware-backed evidence rather than a fingerprint script they control.
The problem
The same computer signs up as 1,000 different people, and your growth numbers, quotas and abuse limits are all quietly counting fakes. The cost lands as inflated metrics you can't trust, free-tier budgets drained by accounts that were never real, and abuse thresholds tripped by a single machine wearing a thousand faces.
Modern signup farms run on cheap inputs: SIM banks at about a cent per phone, disposable email domains at nothing, residential proxies at a few dollars a gigabyte. Fingerprinting catches anyone on stock Chrome and stock Windows and misses anyone using GoLogin, Multilogin, AdsPower or Kameleo — tools whose entire purpose is synthesising plausible “fresh device” profiles on demand.
The thing that doesn't scale is a physical computer. Hardware is the only signup input that costs the attacker more per attempt than the marginal account is worth.
What the device id gives you
- Every signup carries a device id proven by the security chip built into virtually every modern laptop and phone.
- The id is stable per tenant — same device, same id, indefinitely — so “one account per device” is a database check, not a CAPTCHA.
- The id is anonymous: a one-way derivation of the device's hardware key, scoped to you and never the key itself. Nothing links a device across companies, and there is no secret to steal.
- You also see what kind of device it is — discrete chip, firmware TPM, cloud vTPM, or software emulator — so you decide which classes count as real signups.
Bracket your signup handler with verify
Two calls bracket your normal signup: mint a challenge, then verify. A device your policy rejects — a cloud server or an emulator — never gets past the verify. What you add is one verify call and one device-id lookup.
import { RootHerald } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY }); // rh_sk_…
app.post("/api/signup", async (req, res) => {
const { email, password, challengeId, evidence } = req.body;
// Earlier: rh.issueChallenge() -> send the one-time challenge to your app,
// which collects the sealed hardware proof and posts it back here.
const verdict = await rh.verify(evidence, {
challengeId,
policy: "rootherald:builtin:strict-hardware", // real physical chips only
});
if (verdict.device.enrollmentRequired) {
return res.status(409).json({ error: "enrollment_required" });
}
if (verdict.device.verdict !== "pass") {
return res.status(403).json({ error: "device_check_failed" });
}
// Per-tenant device id — stable across this user's future logins, anonymous otherwise.
if (await usersRepo.deviceAlreadyRegistered(verdict.device.ueid)) {
return res.status(409).json({ error: "already_signed_up_from_this_device" });
}
const user = await usersRepo.create({ email, password, deviceId: verdict.device.ueid });
res.json({ ok: true, userId: user.id });
});The client half
The handler above is the server side. Before it runs, the user's device collects the sealed proof through the browser extension or the native SDK. Both client paths are walked end to end in the browser and native guides.
A hard 403 on a failed check will occasionally catch a legitimate user on unusual hardware. Many teams route a failure into an existing step-up flow — add a payment method, verify an email — rather than refusing the signup outright. Under warn you can shadow-limit instead of blocking.