Skip to content
Guide · AI

Keep a free tier without bleeding GPU

Email and phone heuristics don't stop credit farming — disposable inboxes and virtual-number services beat them in seconds, and the cost lands on your inference bill. Gate the free tier on the device: one machine gets one trial, and it survives email and phone churn without a credit-card wall that tanks conversion.

1

Mint a challenge and collect evidence

At free-tier signup, your server mints a single-use challenge with your rh_sk_ secret and relays the nonce. The client quotes the local TPM over it and posts the opaque evidence blob back — no keys on the client, no Root Herald contact.

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 });
});
client — keyless collectorts
import { attest } from "@rootherald/browser";

const { challengeId, nonce } = await fetch("/signup/challenge", { method: "POST" }).then((r) => r.json());
const evidence = await attest(nonce);

await fetch("/signup", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ challengeId, evidence, email, password }),
});
2

Verify server→server and gate the trial on the device id

Appraise the evidence with rh.verify(). On a pass you get device.ueid, stable per tenant — the farmer who churns through 500 throwaway emails on one box still resolves to one id, so they still get one trial.

server — free-tier signupts
app.post("/signup", async (req, res) => {
  const { challengeId, evidence, email, password } = req.body;

  const verdict = await rh.verify(evidence, {
    challengeId,
    policy: "rootherald:builtin:strict-hardware",
  });

  // Treat a non-pass as a DOWNGRADE, not a wall — fall back to card-required.
  if (verdict.device.verdict !== "pass") {
    return res.status(403).json({ error: "device_unverified", retryWith: "payment_method" });
  }

  const prior = await freeTrials.findByDevice(verdict.device.ueid);
  if (prior) return res.status(409).json({ error: "free_trial_already_used_on_device" });

  await freeTrials.create({ deviceId: verdict.device.ueid, email, grantedCredits: FREE_CREDITS });
  res.json({ ok: true });
});

Spinning up cloud VMs is the next-cheapest move, and those classify as cloud-vtpm, which the default strict-hardware policy rejects before you ever see a ueid.

What comes back

The verdict is synchronous — no token to store or verify later. Two fields drive the gate:

AttestationVerdict (trimmed)json
{
  "device": {
    "ueid": "8b1e…",             // one free trial keys off this
    "verdict": "pass",           // pass | warn | fail
    "earStatus": "affirming",
    "attestationType": "tpm20"
  }
}

Why it matters: licensing defensibility

If you're negotiating content licences (music, image, code), "we know our users" matters. This gives you a defensible answer without collecting more PII: each free account maps to a distinct, real device, and you store nothing about that device beyond an opaque per-tenant pseudonym.

Heads up
Treat a failed device check as a downgrade, not a wall: route the user to your existing "add a payment method" flow. You lose nothing — those users were going to hit that flow anyway under email/phone heuristics — and you avoid blocking the occasional legitimate user on an unusual device.