Skip to content
Guide · Embedded game SDK

Integrate the native collector into a game client

A walkthrough for game studios wiring the native SDK into a launcher or matchmaker. Targets Windows (NCrypt PCP), macOS (Secure Enclave), and Linux dedicated servers. Sidesteps the browser and works inside your existing anti-cheat / DRM envelope. Following the Background-Check default, the native client is a dumb collector: it gathers a hardware quote and hands the opaque evidence to your matchmaker, which appraises it server→server with rh.verify().

Where the integration lives

Three places in a typical game stack make sense:

  • Launcher (recommended). Before the launcher hands off to the game executable, collect evidence and call your matchmaker (which appraises it server→server). If the matchmaker rejects, refuse to launch. Lowest friction; highest assurance.
  • First-tick of the game.Boot into a minimal lobby scene, collect, post, refuse to load any map until your matchmaker accepts. Useful if you don't control the launcher (e.g. Steam-distributed without a custom launcher).
  • Matchmaker handshake.Server-side: when the client connects, demand a fresh attestation as part of the join handshake. Works even with mod-supporting / private-server scenarios where launcher attestation doesn't flow through.
1

Link the native SDK

The SDK ships as a static archive (RootHerald.lib on Windows, librootherald.a on Linux/macOS) behind one C99 header, rootherald.h. Link it into your launcher; no runtime DLL/.so to ship.

CMakeLists.txt (launcher)bash
find_package(RootHerald REQUIRED)

add_executable(launcher
  src/main.cpp
  src/herald_gate.cpp)

target_link_libraries(launcher PRIVATE
  RootHerald::RootHerald
  YourGameCore::Core)
2

Collect evidence inside the launcher

RootHeraldClient_CollectEvidence gathers a fresh hardware quote over a nonce your matchmaker minted (via POST /api/v1/attestations/challenge) and writes the opaque evidence JSON into a caller-owned buffer; it consults no API key and never contacts Root Herald. You POST that blob to yourmatchmaker, which appraises it server→server (Step 3) and returns the decision. Following Background-Check, the PASS/WARN/FAIL verdict is your server's decision under your acceptance policy, never the client's.

herald_gate.cppcpp
#include <rootherald.h>
#include <stdexcept>
#include <string>

// Collects the opaque evidence blob over a server-issued nonce. Holds no key
// and never contacts Root Herald — the caller POSTs the blob to your matchmaker,
// which appraises it server→server. Throws on a TPM / collection failure.
std::string CollectEvidenceForLaunch(const char* nonceB64)
{
    char* evidenceJson = nullptr;
    RootHeraldStatus status =
        RootHeraldClient_CollectEvidence(nonceB64, &evidenceJson);

    if (status != ROOTHERALD_OK)
    {
        throw std::runtime_error(
            std::string("rh_collect_failed: ") + RootHerald_ErrorString(status));
    }

    std::string evidence = evidenceJson;       // self-contained evidence blob
    RootHeraldClient_FreeEvidence(evidenceJson); // caller owns + frees the buffer
    return evidence;  // POST verbatim to your matchmaker -> rh.verify()
}
Windows TBS elevation

On Windows firmware that rejects the unprivileged NCrypt/PCP path, the SDK falls back to spawning your executable elevated once with --establish-key <url> <path>. Route that argv to RootHerald_RunElevatedEstablishKey at the very top of main() before any normal startup, as shown in the native SDK's INTEGRATING.md.

3

Appraise the evidence server-side

Your matchmaker mints the challenge, relays the nonce to the launcher (Step 2), receives the opaque evidence, and appraises it server→server with rh.verify() — the verdict comes back directly. Use the stable device.ueid to enforce one-account-per-device, ban lists, or session tickets.

matchmaker (Node)ts
import { RootHerald } from "@rootherald/node";

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

// (a) the launcher first asks for a challenge; relay the nonce to it.
app.post("/matchmake/challenge", async (_req, res) => {
  const { challengeId, nonce } = await rh.issueChallenge();
  res.json({ challengeId, nonce });
});

// (b) the launcher posts back the opaque evidence; appraise it server→server.
app.post("/matchmake/join", async (req, res) => {
  const verdict = await rh.verify(req.body.evidence, {
    challengeId: req.body.challengeId,
    policy: "rootherald:builtin:strict-hardware",
  });
  if (verdict.device.verdict !== "pass")
    return res.status(403).json({ error: "device_rejected" });

  // verdict.device.ueid is the stable per-tenant device id. Enforce
  // one-account-per-device or check a ban list.
  if (await usersRepo.deviceBanned(verdict.device.ueid))
    return res.status(403).json({ reason: "device_banned" });

  res.json({ sessionTicket: makeSessionTicket(verdict.device.ueid) });
});
4

Ban from anti-cheat hits

When your anti-cheat detects cheating, ban the device, not just the account. That's the structural change a hardware-rooted device id enables: ban-evasion requires the cheater to buy a new physical device, not just a new email.

ban workerts
async function banCheater(accountId: string) {
  const user = await usersRepo.find(accountId);
  // user.deviceUeid was persisted from verdict.device.ueid at join time.
  await usersRepo.banDevice({
    ueid: user.deviceUeid,
    reason: `anti-cheat hit: ${user.lastCheatSignal}`,
  });
  await usersRepo.suspend(accountId);
}

Anti-cheat coexistence

Root Herald is complementary to BattlEye / EAC / Vanguard / Ricochet. They attest the process; we attest the device. The right composition:

  • Anti-cheat rejects players whose process looks tampered.
  • Root Herald rejects players whose deviceisn't a real, distinct, ban-list-clean machine.

Together they close both halves of the smurfing problem: smurfs running fresh accounts on the same PC, and smurfs running anti-detect rigs that fake a process to look clean.

Engine-specific bindings

If you ship on Unity or Unreal, the embedded-SDK doc shows the C++ surface, but you probably want the engine-specific bindings instead: Unity plugin (Wave 3), Unreal plugin (Wave 3).