Skip to content
Guide · Games

Smurf & ban-evasion control at signup

Kernel anti-cheat (Vanguard, EAC, BattlEye) watches the running game; it doesn't stop a banned player making a fresh account on the same machine. Bind the account to the device at signup: a hardware identity is far harder to spoof than a disk-serial HWID, and it works before the game even launches.

Where it sits

Root Herald is the identity layer underneath your anti-cheat, not a replacement. Anti-cheat answers "is the running process clean?". Root Herald answers "is this the same device we banned last week?" and "is this one device or 40 smurf accounts on one device?".

1

Mint a challenge and collect evidence in the launcher

On account creation your matchmaker mints a single-use challenge with your rh_sk_ secret and relays the nonce. The launcher quotes the local TPM over it and posts the opaque evidence blob back — no keys on the client.

server — mint the challengets
import { RootHerald } from "@rootherald/node";

const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_… (backend only)

app.post("/signup/challenge", async (_req, res) => {
  const { challengeId, nonce } = await rh.issueChallenge();
  res.json({ challengeId, nonce });
});

Collect on desktop with the native SDK (see the embedded game SDK guide); the launcher hands its opaque evidence blob to /signup with the rest of the form.

2

Verify server→server, then check the ban list and per-device cap

Appraise with rh.verify() and get the verdict back directly. Use the stable device.ueid against your ban list and your per-device account cap, then store it against the new account.

server — signup handlerts
app.post("/signup", async (req, res) => {
  const { challengeId, evidence, ...input } = req.body;

  const verdict = await rh.verify(evidence, {
    challengeId,
    policy: "rootherald:builtin:strict-hardware",
  });
  if (verdict.device.verdict !== "pass") return forbid(res, "device_check_failed");

  const ueid = verdict.device.ueid; // stable per-tenant device id

  if (await banList.hasDevice(ueid)) return forbid(res, "device_banned");

  const count = await accounts.countByDevice(ueid);
  if (count >= MAX_ACCOUNTS_PER_DEVICE) return forbid(res, "too_many_accounts_on_device");

  await accounts.create({ ...input, deviceId: ueid });
  res.json({ ok: true });
});

Optionally re-run the challenge → verify flow on launch and on ranked-match join to catch device swaps mid-life.

What comes back

The verdict is synchronous. Branch on device.verdict; key bans and caps off device.ueid.

AttestationVerdict (trimmed)json
{
  "device": {
    "ueid": "d41c…",             // ban / cap key
    "verdict": "pass",           // pass | warn | fail
    "earStatus": "affirming",
    "attestationType": "tpm20"
  }
}

Pick the policy for your platforms

Use rootherald:builtin:strict-hardware-permissive-mobileif you ship on mobile: it accepts Android's Trusted Execution Environment (TEE — a secure area of the main processor) as well as its stronger StrongBox variant, and macOS Secure Enclave, so legitimate phones aren't false-positived, while still rejecting emulators and cloud vTPMs (the BlueStacks-farm and cloud-gaming-VM cases). Desktop-only? The default strict-hardware is right.

Note
A device that classifies as emulated or mobile-android-software is exactly the smurf-farm signature. Under a strict policy it fails; under a warn-tolerant policy it comes back with verdict: "warn" so you can shadow-ban or queue for review instead of hard-blocking.