Skip to content

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 polls each ANU endpoint on its own schedule and streams what it gets 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": 160,
  "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": "74b033a4332c7112cf0c3ff5...67f6b30ee",
  "timestamp": "2026-08-02T04:14:58Z",
  "source": "quantumnumbers"
}

value is your 1024-bit random number, 256 lowercase hex characters. timestamp is when it was collected from ANU. You will never be given that number again.

There is also a page at http://localhost:8099/ with the same reference and live pool figures.

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 each is polled on its own schedule and their values are merged into the pool, 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
  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

There are no paths in a config. Neither side writes the numbers anywhere, so there is no outbox, spool, or database to name.

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.interval_seconds How often to poll. Default 28800 — eight hours.
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.interval_seconds How often to poll. Default 3600 — one hour.
legacy.size Bytes per block, 1–1024 on this API.

The two APIs cap size differently, and neither comes close to the 128 bytes a value needs. That does not matter: whatever blocks come back are concatenated and re-sliced into 1024-bit values locally. Any leftover bytes are discarded rather than padded, and the count discarded is logged so a badly chosen length/size pair is visible rather than silent.

They also interpret size differently, despite being sent the same type=hex16 request. The keyed API treats it as a count of 16-bit values, so a block is twice size in bytes; the legacy API treats it as a byte count. Both were measured against the live endpoints:

Request Bytes per block Total Values
keyed, length: 1024, size: 10 20 20,480 160
legacy, length: 1024, size: 128 128 131,072 1,024

Since the blocks are concatenated and re-sliced regardless, this affects only how much one request returns, never the values themselves.

Each endpoint is polled on its own schedule, because their rate limits are nothing alike: the keyed API allows roughly 100 requests a month and the legacy one a request a minute. The defaults are sized to those limits — three keyed requests a day is about 90 a month. Raising the keyed frequency much at all will exhaust the monthly quota.

At the defaults the two together supply about 1,000 values an hour.

collect

Field Meaning
retry_limit Send attempts within a cycle before the values are discarded. Default 5.

Nothing is buffered on disk. If the diode will not accept a transfer after retry_limit attempts, those values are dropped — the next cycle collects fresh ones.

diode

Field Meaning
send_command Argument list. The values are written to the command's standard input.
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. There is no {} placeholder: this is a stream, not a file to point at.

Use the stream subcommands

diodetool stream-send and stream-recv, not send and auto-recv. The file-based subcommands take a path and would need somewhere to write it — which is exactly what this program no longer does. --no-wait and -f do not exist on stream-send; passing either makes every cycle fail.

server

Field Meaning
listen Address for the API. Default 127.0.0.1:8099.
max_entries Pool capacity. Default 100000.
served_history How many served values to remember for deduplication. Default 10000.
receive_command Required. Run repeatedly by serve; its output is the arriving stream.

At max_entries: 100000 a full pool is roughly 100 MB of memory, since a value is 128 bytes and is held as hex alongside a dedupe index.

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 / A documentation page with curl examples and live pool figures.
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 memory use stays bounded.

A value is never served twice for as long as the server runs. The pool is held in memory only and is not written anywhere, so a restart starts it empty and it refills from the far side's next cycle. That is deliberate: the numbers are not to be left on disk.

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
Server:       http://127.0.0.1:8099/api/status
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 asks the running server, so it needs serve up — there is no file to read behind its back.

Using a continuously running receiver

diodetool stream-recv emits whatever is queued and exits, which is why serve runs it over and over. If your build has auto-stream-recv — which polls continuously and never exits — it works through the identical code path with no change:

"receive_command": ["diodetool", "auto-stream-recv", "--channel", "quantum", "--no-colour"]

serve simply never gets to restart it. Either is fine; the parser does not care where one invocation ends and the next begins.

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

JSON Lines — one value per line, newline terminated, about 330 bytes each:

{"value":"74b033a4...","timestamp":"2026-08-02T04:14:58Z","source":"quantumnumbers"}
{"value":"3c44fd1d...","timestamp":"2026-08-02T04:14:58Z","source":"quantumnumbers"}

Every value is exactly 256 lowercase hex characters.

There is no framing above the line, deliberately. The transport chops the stream into chunks at arbitrary offsets and the receiving tool hands them over in batches that begin and end wherever they happen to, so a value can be split across two of them. A newline is the only synchronisation point that survives that, and it bounds the damage: a line lost or corrupted in transit costs exactly one value, and parsing resumes at the next newline.

Each line is validated on arrival — value length, lowercase hex, and a parseable timestamp. A line that fails is logged and skipped. A value that has already been seen is ignored, which is what stops a replayed chunk reintroducing numbers that were already served.

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 ANU source tried 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.

gave up after N attempts — the send command is failing and those values have been discarded. Its output is relayed into the log. Nothing is kept: the next cycle collects fresh values.

skipped a damaged line — a value arrived corrupted. A diode has no retransmission, so it cannot be recovered; the stream resynchronises at the next newline and the rest arrives normally.

receive command returned in Xms with no data — the receive tool is not blocking the way it is expected to. Check the channel name and password: a mismatch on either can look like an idle link.

The pool is empty after a restart — expected. The pool is memory only; it refills on the far side's next cycle, which at the default schedules is within the hour.