Skip to content
Guide · Web3

Sybil-resistant airdrops & governance

One person can spin up a thousand wallets to farm an airdrop meant for a thousand people — a sock-puppet attack (a Sybil attack). Anchor eligibility to the device, not the wallet: the security chip in a real machine costs real money to obtain while a fresh wallet costs nothing, so one human with one machine can't farm a thousand allocations. Wallet-graph analytics catch the lazy farms; this catches the ones that rotate fresh wallets behind cloud VMs and proxies.

1

Mint a challenge on your server

When a wallet begins a claim or a vote, your backend mints a single-use challenge with your rh_sk_ secret and relays the nonce to the dApp. The nonce is the anti-replay floor: the client has to quote a live TPM over it.

server — POST /claim/challengets
import { RootHerald } from "@rootherald/node";

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

app.post("/claim/challenge", async (_req, res) => {
  const { challengeId, nonce } = await rh.issueChallenge();
  res.json({ challengeId, nonce }); // relay to the dApp
});
2

Collect evidence on the client

The dApp quotes the local TPM over the nonce and posts the opaque evidence blob back with the wallet address. The collector holds no key and never contacts Root Herald: it only talks to your server.

dApp — keyless collectorts
import { attest } from "@rootherald/browser";

const { challengeId, nonce } = await fetch("/claim/challenge", { method: "POST" }).then((r) => r.json());
const evidence = await attest(nonce); // quotes the TPM over the nonce; no keys on the client

await fetch("/claim", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ challengeId, evidence, wallet }),
});
3

Verify server→server and bind the device to the wallet

Your backend appraises the evidence with rh.verify() and gets the verdict back directly. On a pass you get a stable per-tenant device.ueid — the pseudonym you dedupe on. Bind (device.ueid → wallet) and enforce the rule that matches your distribution: one allocation per device, weight by device-distinct wallets, or flag wallets that share a device.

server — POST /claimts
app.post("/claim", async (req, res) => {
  const { challengeId, evidence, wallet } = req.body;

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

  // One allocation per device. (Or: store every (ueid, wallet) pair and weight at snapshot.)
  const existing = await db.allocations.findByDevice(verdict.device.ueid);
  if (existing && existing.wallet !== wallet) {
    return res.status(409).json({ error: "device_already_claimed", with: existing.wallet });
  }
  await db.allocations.upsert({
    deviceId: verdict.device.ueid,
    wallet,
    attestationType: verdict.device.attestationType,
  });
  res.json({ ok: true });
});

What comes back

rh.verify() returns the verdict synchronously — no token, no JWKS, no offline step. You branch on device.verdict and key everything off device.ueid.

AttestationVerdict (trimmed)json
{
  "acr": "urn:rootherald:device:high",
  "amr": ["hwk"],
  "device": {
    "ueid": "2f9c4a…",            // stable per-tenant device id — dedupe on this
    "verdict": "pass",            // pass | warn | fail
    "earStatus": "affirming",     // affirming | warning | contraindicated
    "attestationType": "tpm20"
  }
}

Keep the strict-hardware policy — it rejects the cloud

A farmer's cheapest move is a fleet of cloud VMs, and a cloud vTPM (AWS NitroTPM, Azure, GCP) is an identity they mint for pennies. The default rootherald:builtin:strict-hardware rejects every cloud-vtpm and emulated class outright, so a NitroTPM-backed claim fails. This is the one use case where you almost never want to loosen it.

Heads up
Crypto-native users expect a thin attest client, but state the privacy posture plainly: Root Herald sees only a hardware-derived pseudonym, never the wallet, never PII, and the pseudonym is uncorrelatable across tenants. That framing keeps you out of the political territory that sank Web Environment Integrity.

Beat pre-snapshot farming: bind continuously

Bind devices on connect and on any reward-relevant action, not just at the snapshot; store the device set over time. A wallet that only ever appears alongside the same device as 200 other wallets is the signal you want, and it's visible even when each wallet's on-chain history looks clean.