Experimental Python random integers backed by Geiger counter decay timing.
pip install nuclear-randomfrom nuclear_random import (
nuclear_choice,
nuclear_randint,
nuclear_random,
nuclear_random_bytes,
service_status,
)
print(nuclear_random(100)) # 0..100 inclusive
print(nuclear_random(10_000)) # 0..10000 inclusive
print(nuclear_randint(10, 20)) # 10..20 inclusive
print(nuclear_random_bytes(16).hex())
print(nuclear_choice(["red", "green", "blue"]))
status = service_status()
print(status.pool_size_bytes, status.estimated_cpm)The public service uses a slow physical source. Calls can wait for fresh extracted bits when the pool is empty.
The public API consumes extracted bits from an ESP32-C3 connected to a Geiger counter on GPIO 6. Each click sends the time since the previous click. The server takes a small number of raw timing bits and runs them through a Von Neumann debiasing extractor before storing full bytes in Redis.
For any request nuclear_random(max_value), the API reads max_value.bit_length() extracted bits and returns the candidate only if it is <= max_value. Candidates above the requested maximum are rejected and the API reads the next bits.
This rejection-sampling step avoids modulo bias.
from nuclear_random import (
nuclear_choice,
nuclear_randint,
nuclear_random,
nuclear_random_bytes,
service_status,
)nuclear_random(max_value)returns an integer in0..max_value.nuclear_randint(min_value, max_value)returns an integer inmin_value..max_value.nuclear_choice(items)returns one item from a non-empty sequence by drawing a QRNG-backed index.nuclear_random_bytes(length)returns extracted bytes from the entropy pool. This is slow on the public service.service_status()returns pool size, click rate, and extractor counters.
The shorter names randint, choice, and random_bytes remain as backward-compatible aliases.
By default the client uses:
https://nuclear-api.datanode.live
Override it while testing:
export NUCLEAR_RANDOM_API_URL=http://127.0.0.1:19000or per call:
nuclear_random(131, api_url="http://127.0.0.1:19000")The server stack is Docker-only and includes:
- FastAPI API
- static public website
- Redis extracted entropy byte pool
- append-only extracted-bit archive on disk
- InfluxDB telemetry
- Wi-Fi ingest endpoint for the ESP32-C3
- USB serial monitor for diagnostics
- status metrics for the future website
- Redis-backed public API rate limiting
cp server/.env.example server/.env
docker compose -p nuclear-random --project-directory server up -d redis influxdb api site
docker compose -p nuclear-random --project-directory server --profile collector up -d collectorSee docs/architecture.md, docs/deployment.md, and docs/hardware.md. See docs/publishing.md for PyPI release steps.
The ESP32-C3 firmware posts click events to https://nuclear-api.datanode.live/v1/entropy/click. Its INGEST_TOKEN must match the server INGEST_TOKEN in /home/server/nuclear_random/server/.env. It should still be built with CDCOnBoot=cdc so the USB serial debug stream is visible on /dev/ttyACM0.
Useful API endpoints:
GET /healthz
GET /v1/status
GET /v1/clicks/timeline
GET /v1/random/int?max=100
GET /v1/random/bytes?length=16
POST /v1/entropy/click
The public website is intended for https://random.datanode.live.
The API also writes extracted bytes into an append-only archive on disk. This is intended for long-term accumulation before running offline statistical batteries such as ENT, dieharder, PractRand, or NIST-style analysis.
Default archive behavior:
- enabled by default
- Docker volume mounted at
/var/lib/nuclear-random/archive - manifest file at
manifest.json - binary shard rotation at
128 MiB
This project is alpha-quality. It uses radioactive decay timing with Von Neumann debiasing, but it has not been cryptographically certified. Throughput is intentionally low because the service does not synthesize extra bytes from each click.
MIT