Docs.
Everything urandom.ai exposes — endpoints per surface, the check protocol, the entropy pipeline, and security posture. One page, no framework.
Console → Examples Goals & stories MCPSurfaces
Auth & rate limits
Only /v1/pqc/* needs a key (it mints key material and is CPU-heavier).
random, entropy, lab, and check are public.
# self-service key — no signup, no identity; 20 per IP per day; shown once POST /v1/keys # -> {"api_key": "..."} curl -XPOST 'https://urandom.ai/v1/keys'
Send it as Authorization: Bearer <key> or X-API-Key. Keys are stored only as
hashes. Every client is rate-limited (≈300 req/min); surfaces are routed by Host.
Core endpoints
# cryptographically secure randomness (SP 800-90A Hash_DRBG, OS-seeded) GET /v1/random/bytes?n=32 # {"hex": "..."} (1..=4096) GET /v1/random/int?min=1&max=6 # {"value": n} unbiased GET /v1/random/uuid # {"uuid": "..."} v4 GET /v1/random/password?length=24 # {"password": "..."} GET /v1/random/passphrase?words=6 # {"passphrase":"...","bits":77.5} diceware (EFF 7776) GET /v1/entropy/health?samples=8192 # SP 800-90B RCT + APT + bit balance curl 'https://urandom.ai/v1/random/bytes?n=16'
PQC endpoints
POST /v1/pqc/ml-kem-768/keypair[?register=true] # FIPS 203 KEM keypair (hex) POST /v1/pqc/ml-dsa-65/keypair[?register=true] # FIPS 204 signing keypair POST /v1/pqc/ml-dsa-65/sign {secret_key,message} # {algorithm,signature} POST /v1/pqc/ml-dsa-65/verify {public_key,message,signature} # {valid} curl -XPOST 'https://pqc.urandom.ai/v1/pqc/ml-kem-768/keypair'
Every response is tagged with its algorithm so a
broken primitive can be retired without changing call sites.
Hybrid X25519MLKEM768 is in the urandom-pqc library (HKDF-SHA256), not an HTTP endpoint.
Lab endpoint
GET /v1/lab/aperiodic?n=10000 # Fibonacci word: never repeats, yet fails monobit # -> proportion_of_ones, monobit_statistic, predictable:true
The check protocol — reveal nothing
A verifiable OPRF (RFC 9497, ristretto255-SHA512, VOPRF mode). The server never sees your key or its hash — only a random-looking blinded point — and proves (DLEQ) it answered with the committed key.
fingerprint = SHA-256(secret) # local, never sent B = r · H(fingerprint) # blinded point — the ONLY thing sent POST /v1/check/evaluate {blinded:B} # -> {evaluated:E=k·B, proof, public_key, epoch} verify DLEQ proof, then y = unblind(E) # abort if the server cheated GET /v1/check/set # opaque registry set (ETag / 304); y in set? POST /v1/check/register {y} # add y (warns future checkers; we never see the key) GET /v1/check/params # ciphersuite, mode, public_key, epoch
Run it three ways: the browser demo at /check,
the CLI (urandom check <key|file>), or the MCP tool (check_key_exposure).
The registry is opt-in (keys you register, or generated with ?register=true) — not a global breach corpus.
Examples — every call, four ways
# 1. mint an API key — no signup, 20/IP/day. Needed only for /v1/pqc/*. KEY=$(curl -s -XPOST https://urandom.ai/v1/keys | jq -r .api_key) # 2. public endpoints need no key curl -s 'https://urandom.ai/v1/random/bytes?n=16' curl -s 'https://urandom.ai/v1/entropy/health?samples=8192' # 3. ML-DSA-65 (FIPS 204) keypair -> sign -> verify (PQC needs the key) H="Authorization: Bearer $KEY"; J="Content-Type: application/json"; B=https://pqc.urandom.ai kp=$(curl -s -XPOST $B/v1/pqc/ml-dsa-65/keypair -H "$H") pub=$(jq -r .public_key <<<"$kp"); sec=$(jq -r .secret_key <<<"$kp") sig=$(curl -s -XPOST $B/v1/pqc/ml-dsa-65/sign -H "$H" -H "$J" \ -d "{\"secret_key\":\"$sec\",\"message\":\"hello\"}" | jq -r .signature) curl -s -XPOST $B/v1/pqc/ml-dsa-65/verify -H "$H" -H "$J" \ -d "{\"public_key\":\"$pub\",\"message\":\"hello\",\"signature\":\"$sig\"}" # {"valid":true}
# pip install requests import requests # 1. mint an API key — no signup, 20/IP/day. Needed only for /v1/pqc/*. key = requests.post("https://urandom.ai/v1/keys").json()["api_key"] # 2. public endpoints need no key requests.get("https://urandom.ai/v1/random/bytes", params={"n": 16}).json() requests.get("https://urandom.ai/v1/entropy/health", params={"samples": 8192}).json() # 3. ML-DSA-65 (FIPS 204) keypair -> sign -> verify (PQC needs the key) h, B = {"Authorization": f"Bearer {key}"}, "https://pqc.urandom.ai" kp = requests.post(f"{B}/v1/pqc/ml-dsa-65/keypair", headers=h).json() sig = requests.post(f"{B}/v1/pqc/ml-dsa-65/sign", headers=h, json={"secret_key": kp["secret_key"], "message": "hello"}).json()["signature"] print(requests.post(f"{B}/v1/pqc/ml-dsa-65/verify", headers=h, json={"public_key": kp["public_key"], "message": "hello", "signature": sig}).json()) # {'valid': True}
// 1. mint an API key — no signup, 20/IP/day. Needed only for /v1/pqc/*. const { api_key: KEY } = await (await fetch("https://urandom.ai/v1/keys", { method: "POST" })).json(); // 2. public endpoints need no key await (await fetch("https://urandom.ai/v1/random/bytes?n=16")).json(); await (await fetch("https://urandom.ai/v1/entropy/health?samples=8192")).json(); // 3. ML-DSA-65 (FIPS 204) keypair -> sign -> verify (PQC needs the key) const B = "https://pqc.urandom.ai"; const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const post = (p, b) => fetch(B + p, { method: "POST", headers: H, body: b && JSON.stringify(b) }).then((r) => r.json()); const kp = await post("/v1/pqc/ml-dsa-65/keypair"); const { signature } = await post("/v1/pqc/ml-dsa-65/sign", { secret_key: kp.secret_key, message: "hello" }); console.log(await post("/v1/pqc/ml-dsa-65/verify", { public_key: kp.public_key, message: "hello", signature })); // { valid: true }
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1" use serde_json::{json, Value}; fn main() -> Result<(), Box<dyn std::error::Error>> { let http = reqwest::blocking::Client::new(); // 1. mint an API key — no signup, 20/IP/day. Needed only for /v1/pqc/*. let minted: Value = http.post("https://urandom.ai/v1/keys").send()?.json()?; let auth = format!("Bearer {}", minted["api_key"].as_str().unwrap()); // 2. public endpoints need no key http.get("https://urandom.ai/v1/random/bytes?n=16").send()?.json::<Value>()?; http.get("https://urandom.ai/v1/entropy/health?samples=8192").send()?.json::<Value>()?; // 3. ML-DSA-65 (FIPS 204) keypair -> sign -> verify (PQC needs the key) let b = "https://pqc.urandom.ai"; let kp: Value = http.post(format!("{b}/v1/pqc/ml-dsa-65/keypair")) .header("Authorization", &auth).send()?.json()?; let (sk, pk) = (kp["secret_key"].as_str().unwrap(), kp["public_key"].as_str().unwrap()); let signed: Value = http.post(format!("{b}/v1/pqc/ml-dsa-65/sign")).header("Authorization", &auth) .json(&json!({ "secret_key": sk, "message": "hello" })).send()?.json()?; let sig = signed["signature"].as_str().unwrap(); let ok: Value = http.post(format!("{b}/v1/pqc/ml-dsa-65/verify")).header("Authorization", &auth) .json(&json!({ "public_key": pk, "message": "hello", "signature": sig })).send()?.json()?; println!("{ok}"); // {"valid":true} Ok(()) }
Heads-up: the sign endpoint takes your
secret_key over the wire and keypair mints it server-side — handy for testing, but for real
signing keep the secret on your machine: use the urandom-pqc Rust crate (or the WASM build) and never send it.
Entropy & randomness
- Primary source.
fill_secure()= the OS CSPRNG (getrandom→getrandom(2)/getentropy) seeding a SP 800-90A Hash_DRBG (SHA-256, zeroized state). The trust root is the kernel CSPRNG. - Health-tested. SP 800-90B Repetition-Count + Adaptive-Proportion tests; the
urandom-labSP 800-22 battery emits real p-values. - Honesty. The seed is a multi-source SHA-256 mix — OS CSPRNG (the trust root) + CPU timing-jitter + x86 RDSEED (M13's "wall of entropy"). The collision-resistant combiner keeps it at least as strong as the OS source alone; no remote/client timing is ever folded in. Von Neumann debiasing exists in
urandom-corebut isn't wired into this path. - Aperiodic ≠ random. Passing statistical tests can falsify quality, never certify unpredictability. See the exhibit.
Security posture
- Auth. Only
/v1/pqc/*needs a key (self-service viaPOST /v1/keys, stored as a hash); the rest is public, rate-limited. - Transport. TLS everywhere, with HSTS, CSP (
wasm-unsafe-evalfor the in-browser demo), nosniff, and frame-DENY response headers. - Memory.
forbid(unsafe_code)workspace-wide; secret keys, seeds, and DRBG state zeroized on drop. - Validated. DRBG vs NIST CAVP; PQC vs FIPS KATs; the OPRF vs the RFC 9497 vectors.