Usage
Setup
You need two machines: one with internet access, one behind the diode. Run one command on each.
Machine 1 — internet side
uv tool install wj-diode-quantum
wj-diode-quantum setup send
wj-diode-quantum collect -c config.json
setup send asks for your ANU API key and your diode's details, then writes everything it needs.
Leave collect running — it collects on a schedule and pushes each batch across the diode.
Machine 2 — protected side
uv tool install wj-diode-quantum
wj-diode-quantum setup recv
wj-diode-quantum serve -c config.json
serve runs the receiving tool for you, so this is the only process you need on this machine.
Check it works
curl localhost:8099/api/status
{
"available": 640,
"capacity": 100000,
"oldest": "2026-08-02T04:14:58Z",
"newest": "2026-08-02T04:14:58Z",
"served_total": 0,
"last_ingest": "2026-08-02T04:15:12Z"
}
Then take a number:
curl localhost:8099/api/random
{
"value": "a3f1c8d20b74e569ff10c3a8827d4e61",
"timestamp": "2026-08-02T04:14:58Z",
"source": "quantumnumbers"
}
value is your 128-bit random number. timestamp is when it was collected from ANU. You will never
be given that number again.
That is the whole setup. Everything below is reference material you only need if you are changing something.
Reference
Getting an ANU API key
The current ANU API needs a free account. Sign up and generate a key at
quantumnumbers.anu.edu.au/api-key, then give it to
setup send when asked.
The older endpoint at qrng.anu.edu.au needs no key, but it is limited to one request per minute
and ANU are retiring it. It works as a fallback or for trying things out. Both can be enabled at
once, in which case both are used on every cycle and their values are merged into the same batch,
each tagged with where it came from.
Commands
wj-diode-quantum — collects quantum random numbers from ANU, pushes them across a
data diode, and serves them one at a time on the far side.
quick start:
On the internet-connected machine:
1. wj-diode-quantum setup send — answer a few questions
2. wj-diode-quantum collect -c CONFIG — start collecting
On the protected machine:
1. wj-diode-quantum setup recv — answer a few questions
2. wj-diode-quantum serve -c CONFIG — start serving
commands:
setup Interactive wizard; writes a working config (send or recv)
collect Collect from ANU and push batches over the diode
serve Receive batches, store them, and serve numbers over HTTP
ingest Queue a received batch (called by the diode's exec hook)
stats Print pool depth, age range, and served totals
sample-config Print a full example config with every option
flags:
--version Show version and exit
--license Show licence information and exit
--help Show this help
Run wj-diode-quantum COMMAND --help for command-specific help.
Configuration
setup writes a minimal config. To see every available option:
wj-diode-quantum sample-config
Relative paths inside a config are resolved against the config file's own directory, so a config and its data directory can be moved together.
anu
| Field | Meaning |
|---|---|
timeout_seconds |
How long to wait for an ANU response. Default 60. |
quantumnumbers.enabled |
Use the keyed API. |
quantumnumbers.api_key |
Your API key. Required when that source is enabled. |
quantumnumbers.length |
Blocks per request, 1–1024. |
quantumnumbers.size |
16-bit values per block, 1–10 on this API. A block is twice this in bytes. |
legacy.enabled |
Use the old unkeyed API. |
legacy.length |
Blocks per request, 1–1024. |
legacy.size |
16-bit values per block, 1–1024 on this API. |
The two APIs cap size differently, and neither is guaranteed to match the 16 bytes a value needs.
That does not matter: whatever blocks come back are concatenated and re-sliced into 128-bit values
locally. Any leftover bytes are discarded rather than padded.
Requests are made with type=hex16, so size counts 16-bit values per block rather than bytes — a
block at size: 10 is 20 bytes. At length: 1024, size: 10 the keyed API therefore yields 20,480
bytes, or 1,280 values per request.
collect
| Field | Meaning |
|---|---|
interval_seconds |
How often to collect. Default 3600. |
outbox_dir |
Where batches wait until the diode accepts them. |
retry_limit |
Send attempts before a batch is moved to outbox/failed/. Default 5. |
diode
| Field | Meaning |
|---|---|
send_command |
Argument list. Exactly one argument must contain {}, replaced with the batch path. |
timeout_seconds |
How long a send may take. Default 600. |
The command is run directly, not through a shell, so arguments containing spaces need no quoting.
Pass --no-wait to diodetool
By default diodetool send blocks after uploading, waiting for the receiving side to consume the
transfer. On a genuinely unidirectional link that acknowledgement can never arrive, so the send
hangs forever. setup adds --no-wait for you.
server
| Field | Meaning |
|---|---|
listen |
Address for the API. Default 127.0.0.1:8099. |
database |
The pool file. |
spool_dir |
Where ingest queues arriving batches. |
max_entries |
Pool capacity. Default 100000. |
served_history |
How many served values to remember. Default 10000. |
spool_poll_seconds |
How often to check the spool. Default 5. |
receive_command |
Optional. Run and supervised by serve. |
The API
There is no authentication. Anyone who can reach the port can drain the pool. Keep it on loopback or a trusted network, or put a reverse proxy in front of it.
| Endpoint | Returns |
|---|---|
GET /api/random |
One value and its collection timestamp. 503 with {"error":"pool empty"} when drained. |
GET /api/status |
Pool depth, timestamp range, totals. |
GET /api/version |
The running version. |
One value per request, by design.
How the pool behaves
The pool is a bounded LIFO. Requests are served the newest value first, and once the pool
reaches max_entries the oldest values are discarded to make room. It is a sliding window over the
most recent numbers, so served timestamps stay close to collection time and the file never grows
without limit.
A value is never served twice, and that holds across a crash: each take is written to a journal and flushed to disk before the value is sent to the client. If the process dies, the journal is replayed at startup and the consumed values do not come back.
Because a diode carries nothing back to the sending side, the collector can never learn that the
server is running low. Watch /api/status or run stats:
wj-diode-quantum stats -c config.json
Database: /var/lib/wj-diode-quantum/pool.json
Available: 638 of 100000 (0.6% full)
Served total: 2
Newest value: 2026-08-02T04:14:58Z (12 minutes ago)
Oldest value: 2026-08-02T04:14:58Z (12 minutes ago)
Last ingest: 2026-08-02T04:15:12Z (11 minutes ago)
stats only reads, so it is safe to run while the server is going.
Running without serve supervising the receiver
Leave receive_command out of the config and run the receiving tool yourself. It should call
ingest for each transfer:
diodetool auto-recv --temp-out \
--exec 'wj-diode-quantum ingest -c /etc/wj-diode-quantum/config.json "{}"'
ingest validates the batch and copies it into the spool, exiting 0 only once it is durably
queued — which matters, because --temp-out deletes the extracted directory as soon as the hook
returns. It needs no lock on the server and works fine when the server is down; batches simply
accumulate in the spool and merge at the next start.
Under systemd
[Unit]
Description=wj-diode-quantum
After=network-online.target
[Service]
ExecStart=/usr/local/bin/wj-diode-quantum serve -c /etc/wj-diode-quantum/config.json
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Use collect in place of serve on the sending machine.
What crosses the diode
One JSON file per collection cycle:
{
"version": 1,
"batch_id": "9f4c1ab27d3e5580",
"created": "2026-08-02T04:15:00Z",
"count": 640,
"entries": [
{ "value": "a3f1c8...", "timestamp": "2026-08-02T04:14:58Z", "source": "quantumnumbers" }
]
}
Every value is exactly 32 lowercase hex characters. Filenames include the creation time and the batch identifier, because diodetool delivers bare basenames and silently overwrites collisions.
A batch is validated whole on arrival — version, value format, timestamps, declared count, and
duplicates. Anything that fails is rejected entirely and moved to spool/bad/ rather than partially
imported. A batch that arrives twice is recognised by its identifier and adds nothing.
Troubleshooting
pool empty — nothing has arrived yet, or the pool has been drained. Check stats on this
machine and the collector's log on the other.
every enabled ANU source failed: legacy — usually the legacy endpoint's one-request-per-minute
limit. It returns HTTP 500 with a plain-text explanation, which appears in the log.
Batches pile up in outbox/ — the send command is failing. Its output is relayed into the log.
After retry_limit attempts a batch moves to outbox/failed/.
Files appear in spool/bad/ — a batch arrived corrupted. A diode has no retransmission, so it
cannot be recovered; the file is kept for inspection.