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.
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.
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 });
});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 }),
});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.
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:
{
"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.