The threat model#
Anyone with the admin private key can authorize any recipient for any amount. Splitting that key from the operator's warm wallet gives you defense-in-depth: warm-wallet compromise lets an attacker send funds out, but not authorize new claims.
The clone's EIP-712 domain is baked into immutable args at deploy. The signer just needs to recover to a holder of CLAIM_AUTHORIZER_ROLE at the time the recipient submits.
1. Sign on the server#
signClaimAuthorization from @tokenops/sdk/fhe-airdrop is a pure function, give it a viem Account + the recipient + encrypted amount + nonce, get back a 65-byte signature.
// server/sign-claim.ts, runs in a server action or Node worker.
import { privateKeyToAccount } from "viem/accounts";
import { signClaimAuthorization } from "@tokenops/sdk/fhe-airdrop";
const admin = privateKeyToAccount(process.env.AIRDROP_ADMIN_PK as `0x${string}`);
// nonce uniqueness is the operator's responsibility, use a monotonic counter
// per recipient or a UUID-derived bytes32.
export async function buildClaimAuthorization({
recipient,
encryptedAmount,
nonce,
}: {
recipient: `0x${string}`;
encryptedAmount: `0x${string}`;
nonce: `0x${string}`;
}) {
const signature = await signClaimAuthorization({
account: admin,
publicClient, // for chainId
airdropAddress,
recipient,
encryptedAmountHandle: encryptedAmount,
nonce,
});
return { recipient, encryptedAmount, nonce, signature };
}2. Submit on the client#
The recipient page fetches the signature bundle from your server action / API and submits via useAirdropClaim— same TanStack Query envelope as any other write hook.
// components/ClaimButton.tsx
const claim = useAirdropClaim({ address: airdropAddress });
// Pull the signature bundle from your server action / API.
const auth = await fetch("/api/sign-claim?recipient=" + address).then((r) => r.json());
claim.mutate({
recipient: auth.recipient,
encryptedAmount: auth.encryptedAmount,
signature: auth.signature,
nonce: auth.nonce,
});Pre-flight before submitting#
Use useAirdropIsSignatureValid + useAirdropIsSignatureClaimed as a free off-chain check before the recipient pays gas. Both are fast reads; they catch typos in nonce / signature shape long before the wallet modal opens.