Agents keep needing a random number that another party can check: picking a reviewer, breaking a tie, sampling a test case, running a small raffle. Math.random() works until someone asks "prove you didn't reroll."
drand already fixes this. It's a public randomness beacon run by a group of independent organizations (the League of Entropy). Every 3 seconds its quicknet chain publishes a round: a BLS signature over the round number, plus randomness = SHA-256(signature). Anyone holding the chain's public key can check a round offline.
The catch is that most agents don't call drand directly. They call some HTTP API that relays it. Then the question is whether you trust the relay.
You shouldn't have to. Here's how to take a beacon from a relay and verify it yourself before you use it.
The relay
I'll use Proof Random API, a small free relay. It returns the latest quicknet round as flat JSON:
curl -sS 'https://proof-random-api.pn-26f.workers.dev/v1/random?nonce=my-request-1&max=6'
{ "value": 2, "range": [0, 6], "nonce": "my-request-1",
"chainHash": "…quicknet chain hash…", "round": 12345678,
"randomness": "…64 hex…", "signature": "…96 hex…",
"signatureDigestMatches": true, "blsVerified": false,
"counter": 0, "algorithm": "SHA-256(chainHash:round:randomness:nonce:counter), uint32be, rejection sample",
"x402": false }
The value is an integer in [0,max) (here 0 through 5), derived by SHA-256 and rejection sampling from the beacon and nonce. The response above is illustrative, not a live round. signatureDigestMatches: true only means the relay checked SHA-256(signature) == randomness. blsVerified: false is the important limit: it does not check the BLS signature. Verification happens on your side, against keys you pin yourself. The free endpoint has no x402 payment.
Pin the chain, not the response
Never read the public key from the thing you're verifying. Hard-code quicknet's chain hash and public key (both published by drand):
const CHAIN = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971';
const PUBKEY = '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a';
Verify the same round independently
Install the official client: npm i drand-client. Fetch the relay's round, then fetch and BLS-verify that same round from drand's own HTTP endpoint, and compare:
import { HttpCachingChain, HttpChainClient, fetchBeacon } from 'drand-client';
export async function getVerifiedBeacon(nonce) {
const r = await fetch('https://proof-random-api.pn-26f.workers.dev/v1/random?nonce=' + encodeURIComponent(nonce) + '&max=6');
if (!r.ok) throw new Error('relay HTTP ' + r.status);
const data = await r.json();
if (data.nonce !== nonce || data.chainHash !== CHAIN || data.range?.[0] !== 0 || data.range?.[1] !== 6)
throw new Error('unexpected relay parameters');
const opts = { disableBeaconVerification: false, noCache: false,
chainVerificationParams: { chainHash: CHAIN, publicKey: PUBKEY } };
const client = new HttpChainClient(new HttpCachingChain('https://drand.cloudflare.com/' + CHAIN, opts), opts);
const independent = await fetchBeacon(client, data.round); // checks BLS sig + SHA-256(sig)
if (independent.signature !== data.signature || independent.randomness !== data.randomness)
throw new Error('relay beacon does not match verified drand beacon');
return { ...data, clientVerified: true };
}
The independent client checks the same round and its BLS signature. Comparing it with the relay means a forged beacon fails; a relay can still choose or repeat a genuine round. You must also check the sampled value against the verified beacon, rather than accepting the relay's value on its word.
Turn it into an unbiased integer
randomness % 6 is biased. Reproduce the relay's SHA-256 and rejection sampling from the verified beacon and your nonce, then compare its value:
import { createHash, randomUUID } from 'node:crypto';
export function sampleInteger(b, max) {
if (!b.clientVerified) throw new Error('verify the beacon first');
if (!Number.isSafeInteger(max) || max < 1 || max > 65536) throw new Error('invalid max');
const bound = Math.floor(2 ** 32 / max) * max;
for (let i = 0; i < 100; i++) {
const raw = createHash('sha256')
.update(`${CHAIN}:${b.round}:${b.randomness.toLowerCase()}:${b.nonce}:${i}`, 'utf8')
.digest().readUInt32BE(0);
if (raw < bound) return { value: raw % max, counter: i };
}
throw new Error('sampling exhausted');
}
const b = await getVerifiedBeacon(randomUUID());
const local = sampleInteger(b, 6);
if (b.value !== local.value || b.counter !== local.counter)
throw new Error('relay sampled value does not match the verified beacon');
console.log(local.value); // a die roll anyone can recompute
Anyone with the verified round, nonce and this function gets the same number. The repository's full verified-client.mjs independently verifies BLS, but its sample function does not automatically compare the returned value; the explicit comparison above matters.
The honest limit
Verification proves the number came from drand. It does not prove you didn't shop for a round. If you call, see a 2, and call again, the latest-round endpoint won't stop you.
For a draw that has to be fair against you, all parties agree on a future round number and the nonce before that round is published, then everyone verifies that round. The free relay only serves the latest round today, so use it for sampling and tie-breaks, not for prizes.
Code
The full client (input checks and timeouts included) is verified-client.mjs in the repo: https://github.com/kepler-ops-maker/proof-random-api
This relay is a free prototype. There's no payment and no account, and it doesn't claim to be a VRF. If you'd rather skip the relay, point fetchBeacon at drand directly. The verification code stays the same.













