{"openapi":"3.1.0","info":{"title":"Geiger TRNG API","description":"\nTrue random bytes from a CAJOE Geiger counter on a Pi 4. Bits are\nextracted from inter-arrival times of radioactive decay events\n(Δt₁ vs Δt₂ comparison → unbiased by symmetry), SHA-256-conditioned\nby default, and served at ~75 bytes/min sustained.\n\n### Project documentation\n\n- [README / project guide (CLAUDE.md)](https://github.com/tinymachines/geiger/blob/main/CLAUDE.md)\n- [Latest checkpoint](https://github.com/tinymachines/geiger/blob/main/docs/CHECKPOINT.md) — current state, rate, wiring\n- [START-HERE handoff doc](https://github.com/tinymachines/geiger/blob/main/docs/START-HERE.md) — full theory + bench history\n\n### Integrating\n\n- [JS client library (`hotbits.js`)](https://github.com/tinymachines/geiger/blob/main/webapp/README.md) — drop-in for browsers, bun, Node 22+\n- [UI port plan (`HOTBITS-UI-PLAN.md`)](https://github.com/tinymachines/geiger/blob/main/docs/HOTBITS-UI-PLAN.md) — six mini-apps mapped to these endpoints\n\n### Internals\n\n- [Scope CLI guide](https://github.com/tinymachines/geiger/blob/main/docs/SCOPE-GUIDE.md) — Rigol DS1054Z control\n- [Test-suite scaffolding](https://github.com/tinymachines/geiger/blob/main/extern/README.md) — PractRand / NIST STS / TestU01\n- [RNG test-suite reference](https://github.com/tinymachines/geiger/blob/main/docs/trng/rng-test-suites.md)\n- [Source on GitHub](https://github.com/tinymachines/geiger)\n\n### Endpoints below\n\n`/random/*` are rate-limited to 30/min per IP; `/health` and `/stats` are 120/min. CORS is open by default for trusted-LAN deploys (`GEIGER_CORS_ORIGINS=*`). The `oversample` query parameter on `/random/bytes` and `/random/hex` tunes the SHA-256 input/output ratio (default 2.0, NIST minimum 1.5).\n","contact":{"name":"Source","url":"https://github.com/tinymachines/geiger"},"version":"0.4.0"},"paths":{"/health":{"get":{"summary":"Health","description":"Liveness probe: 200 if everything's healthy, 503 otherwise.\n\nHealthy = logger service active + events.csv fresh + pool has at\nleast 1 byte ready (not necessarily the full low-water amount).","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/stats":{"get":{"summary":"Stats","operationId":"stats_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/analysis/run":{"post":{"summary":"Analysis Run","description":"Kick off an analysis run in the background. Returns immediately\nwith a job_id; poll `/analysis/jobs/{job_id}` for status.","operationId":"analysis_run_analysis_run_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisRunRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analysis/jobs":{"get":{"summary":"Analysis Jobs","description":"List in-memory job records (last ~100 runs since process start).","operationId":"analysis_jobs_analysis_jobs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/analysis/jobs/{job_id}":{"get":{"summary":"Analysis Job","operationId":"analysis_job_analysis_jobs__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analysis/runs":{"get":{"summary":"Analysis Runs","description":"List the last N output directories on disk (regardless of which\nprocess produced them). Newest first.","operationId":"analysis_runs_analysis_runs_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"N"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/longterm":{"get":{"summary":"Longterm","description":"Live Section H — long-term drift, diurnal cycle, outlier episodes,\nFano-factor summary, and incident timeline. Computed from the current\nL2 history (~1 s), cached for 60 s.","operationId":"longterm_longterm_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/longterm/diurnal":{"get":{"summary":"Longterm Diurnal","description":"Just the hour-of-day rate profile (24 bins, mean ± SEM, n) plus\nthe ANOVA verdict — a compact endpoint for the diurnal viz.","operationId":"longterm_diurnal_longterm_diurnal_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/incidents":{"get":{"summary":"Incidents","description":"The bench incident log (`incidents.md`) parsed to structured JSON:\ntitle, start_utc, end_utc, kind, and whether the window masks data.\nFor overlaying incident bands on time-series charts.","operationId":"incidents_incidents_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/events":{"get":{"summary":"Events","description":"SSE broadcast of bench analysis events (see module comment above).\n\nExample (browser):\n    const es = new EventSource(\"http://192.168.1.118:8000/events\");\n    es.addEventListener(\"health_alert\", e => alarm(JSON.parse(e.data)));\n    es.addEventListener(\"metrics\", e => plot(JSON.parse(e.data)));","operationId":"events_events_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/continuous":{"get":{"summary":"Health Continuous","description":"L1 continuous-health snapshot: current RCT + APT state from\n`stream_state.json`, plus a count of FAIL rows in `health.csv`\nover the last 24 h. Always 200 — the `failed` flags carry the\npass/fail signal.","operationId":"health_continuous_health_continuous_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/metrics":{"get":{"summary":"Metrics","description":"L2 rolling-stats trend. Returns metrics.tsv rows newer than `since`,\neach row coerced to native types. Cap: 30d window.","operationId":"metrics_metrics_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"string","description":"time window: 30m / 1h / 24h / 7d / etc. Max 30d.","default":"1h","title":"Since"},"description":"time window: 30m / 1h / 24h / 7d / etc. Max 30d."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/metrics/latest":{"get":{"summary":"Metrics Latest","description":"The most recent L2 row, or `null` if metrics.tsv is empty.","operationId":"metrics_latest_metrics_latest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/battery/history":{"get":{"summary":"Battery History","description":"L3 daily-battery history — the trailing N rows of\nbattery_history.tsv.","operationId":"battery_history_battery_history_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"last N daily verdicts (default 30, max 365)","default":30,"title":"N"},"description":"last N daily verdicts (default 30, max 365)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/timeseries/{source}":{"get":{"summary":"Timeseries","description":"Generic time-series accessor over any L2/L3 TSV-backed source.\n\nExamples:\n    /timeseries/metrics?since=24h\n    /timeseries/sensors?since=7d&columns=temp_c,volts_core\n    /timeseries/dispersion?since=all&limit=5000\n    /timeseries/battery?since=30d\n    /timeseries/health?since=1h","operationId":"timeseries_timeseries__source__get","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}},{"name":"since","in":"query","required":false,"schema":{"type":"string","description":"30m / 1h / 7d / 'all' (≤ 365d). Sources: ['battery', 'dispersion', 'experiments', 'health', 'metrics', 'sensors'].","default":"1h","title":"Since"},"description":"30m / 1h / 7d / 'all' (≤ 365d). Sources: ['battery', 'dispersion', 'experiments', 'health', 'metrics', 'sensors']."},{"name":"columns","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"comma-separated column projection (ts_iso always kept)","title":"Columns"},"description":"comma-separated column projection (ts_iso always kept)"},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":100000,"minimum":1},{"type":"null"}],"description":"cap the response to the trailing N rows after filter","title":"Limit"},"description":"cap the response to the trailing N rows after filter"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensors":{"get":{"summary":"Sensors","description":"L2 Pi-sensor companion to /metrics (joined on ts_iso).","operationId":"sensors_sensors_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"string","description":"duration window (30m / 1h / 7d / 'all')","default":"1h","title":"Since"},"description":"duration window (30m / 1h / 7d / 'all')"},{"name":"columns","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Columns"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":100000,"minimum":1},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dispersion":{"get":{"summary":"Dispersion","description":"L2 Fano-factor companion to /metrics — D=Var/Mean of event counts\nat τ=1 s and τ=60 s over trailing windows. For pure Poisson D=1.0;\ndepartures fingerprint after-pulses, drift, or dead-time clipping.\nSee docs/scope-grabs/2026-05-26-dispersion-spectrum.png.","operationId":"dispersion_dispersion_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"string","description":"duration window (30m / 1h / 7d / 'all')","default":"1h","title":"Since"},"description":"duration window (30m / 1h / 7d / 'all')"},{"name":"columns","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Columns"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":100000,"minimum":1},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/docs/scope-grabs":{"get":{"summary":"Docs Grabs Index","description":"Simple HTML listing of docs/scope-grabs/ — mirrors nginx autoindex.\nLinked from the bench landing page.","operationId":"docs_grabs_index_docs_scope_grabs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Docs Grabs Index","description":"Simple HTML listing of docs/scope-grabs/ — mirrors nginx autoindex.\nLinked from the bench landing page.","operationId":"docs_grabs_index_docs_scope_grabs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/docs/scope-grabs/":{"get":{"summary":"Docs Grabs Index","description":"Simple HTML listing of docs/scope-grabs/ — mirrors nginx autoindex.\nLinked from the bench landing page.","operationId":"docs_grabs_index_docs_scope_grabs__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Docs Grabs Index","description":"Simple HTML listing of docs/scope-grabs/ — mirrors nginx autoindex.\nLinked from the bench landing page.","operationId":"docs_grabs_index_docs_scope_grabs__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/docs/scope-grabs/{filename}":{"get":{"summary":"Docs Grab File","description":"Serve an individual scope grab. Only image extensions are allowed.","operationId":"docs_grab_file_docs_scope_grabs__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Docs Grab File","description":"Serve an individual scope grab. Only image extensions are allowed.","operationId":"docs_grab_file_docs_scope_grabs__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/docs/{filename}":{"get":{"summary":"Docs Top Level File","description":"Serve a top-level docs file (README-style markdown / images).\n\nNote: FastAPI registers /docs (no path) as Swagger UI. This handler\nruns for /docs/<filename> where <filename> is a single path segment,\nso /docs alone still hits Swagger. Only specific extensions allowed.","operationId":"docs_top_level_file_docs__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Docs Top Level File","description":"Serve a top-level docs file (README-style markdown / images).\n\nNote: FastAPI registers /docs (no path) as Swagger UI. This handler\nruns for /docs/<filename> where <filename> is a single path segment,\nso /docs alone still hits Swagger. Only specific extensions allowed.","operationId":"docs_top_level_file_docs__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/random/bytes":{"get":{"summary":"Random Bytes","description":"SHA-256-conditioned random bytes (default secure path).","operationId":"random_bytes_random_bytes_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":4096,"minimum":1,"default":32,"title":"N"}},{"name":"oversample","in":"query","required":false,"schema":{"type":"number","maximum":8.0,"minimum":1.0,"description":"Entropy-input-to-output ratio for SHA-256 conditioning. NIST SP 800-90B recommends ≥ 1.5 for full-entropy output; 2.0 is the default. Lower values reduce pool drain.","default":2.0,"title":"Oversample"},"description":"Entropy-input-to-output ratio for SHA-256 conditioning. NIST SP 800-90B recommends ≥ 1.5 for full-entropy output; 2.0 is the default. Lower values reduce pool drain."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/random/raw":{"get":{"summary":"Random Raw","description":"Raw unconditioned bytes from the pool. For analysis / experimentation.\n\nSets `X-Geiger-Conditioning: none` so callers know.","operationId":"random_raw_random_raw_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":4096,"minimum":1,"default":32,"title":"N"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/random/archive":{"get":{"summary":"Random Archive","description":"NON-CRYPTOGRAPHIC bytes read straight from bits_stream.bin at an\noffset, WITHOUT consuming the pool — no watermark advance, no low-water\ngate, no L1 APT gate.\n\nFor visualizations / demos that need a steady trickle of looks-random\nbytes but must not drain the scarce fresh tip that `/random/bytes`\nserves real consumers. The L1 stream is append-only, so this replays\nalready-emitted bits: they may have been served before and are\ndeterministic given (offset, n). Do NOT use as fresh randomness — the\n`X-Geiger-Archive: replay` header marks that. Reads below the current\nfile size, so it never 503s on a low pool (only if the file is missing).","operationId":"random_archive_random_archive_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":4096,"minimum":1,"default":256,"title":"N"}},{"name":"offset","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Byte offset into bits_stream.bin. Omit for a uniform random start. Result is deterministic given (offset, n).","title":"Offset"},"description":"Byte offset into bits_stream.bin. Omit for a uniform random start. Result is deterministic given (offset, n)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/random/hex":{"get":{"summary":"Random Hex","description":"SHA-256-conditioned random bytes, hex-encoded as JSON.","operationId":"random_hex_random_hex_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":4096,"minimum":1,"default":32,"title":"N"}},{"name":"oversample","in":"query","required":false,"schema":{"type":"number","maximum":8.0,"minimum":1.0,"default":2.0,"title":"Oversample"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/random/integers":{"get":{"summary":"Random Integers","description":"Random integers in [min, max] via rejection sampling.\n\nUses raw pool bytes (not conditioned) — rejection sampling preserves\nuniformity from a uniform input, and the pool is already passing all\nof ent / PractRand / Rabbit / Alphabit.","operationId":"random_integers_random_integers_get","parameters":[{"name":"n","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":10,"title":"N"}},{"name":"min","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Min"}},{"name":"max","in":"query","required":false,"schema":{"type":"integer","default":99,"title":"Max"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AnalysisRunRequest":{"properties":{"bits_window_bytes":{"type":"integer","maximum":8388608.0,"minimum":1024.0,"title":"Bits Window Bytes","default":65536},"events_window_minutes":{"type":"number","maximum":1440.0,"minimum":1.0,"title":"Events Window Minutes","default":60.0},"metrics_window_hours":{"type":"number","maximum":720.0,"minimum":1.0,"title":"Metrics Window Hours","default":24.0},"pileup_us":{"type":"number","maximum":1000000.0,"minimum":0.0,"title":"Pileup Us","default":5000.0},"max_acf_lag":{"type":"integer","maximum":256.0,"minimum":1.0,"title":"Max Acf Lag","default":32},"label":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Label"},"keep_raw":{"type":"boolean","title":"Keep Raw","default":true},"keep_png":{"type":"boolean","title":"Keep Png","default":true}},"type":"object","title":"AnalysisRunRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}