# wai-quantum-service The WAI quantum stack as a public job service. Send an OpenQASM program; get back measurement counts, Pauli expectation values or amplitudes. Each result names the method that produced it and how accurate it is, and carries a signature you can check. No account and no key. The public instance is at **https://wai.transaction.science/quantum-jobs/v1**. It keeps every job across its own restarts for a day, runs sets of programs and sweeps of one program's inputs as one job, and answers a dry run with its plan before anything runs. You can run the same service yourself: [docs/self-host.md](docs/self-host.md). A service started with a model also writes programs: send it a task, what a program must do, and its own model writes an OpenQASM program for it, the engine checking each attempt ([below](#writing-a-program-for-a-task)). The service runs jobs on its own engine only. It never routes a job to anyone's quantum hardware or provider and never takes a credential: a job that names a `provider` is refused (`400`), with where such a run goes instead (the command line's `run --provider`, or the desktop app, which hold the person's own credentials; see `wai-quantum-qpu/bring-your-own-quantum.md`). ```sh runnable needs=service # A Bell pair, 1000 shots. curl -s https://wai.transaction.science/quantum-jobs/v1/run \ -H 'content-type: application/json' \ -d '{"qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 1000}' ``` ## Jobs A job is JSON: | field | meaning | |---|---| | `qasm` | the program: OpenQASM 3, or OpenQASM 2 (required, unless `source` gives it) | | `source` | the program in another format, in place of `qasm`: `{"format": "qir", "program": "…"}` (QIR as LLVM text) or `{"format": "jeff", "program_base64": "…"}`; read to the job's `qasm` before anything else reads the job, the request keeping its source, and a module that does not read refused with its code (`Q700` to `Q721`) and place | | `method` | `auto` (the default) or a method below | | `shots` | measurement shots (default 1024 when nothing else is asked for; at most 1,000,000) | | `seed` | the sampling seed (default 1); the same job and seed give the same counts | | `observables` | Pauli strings such as `"Z0 Z1"`, or `{"pauli": "X3 Y4", "coeff": 0.5}` | | `amplitudes` | bit strings, one character per qubit, qubit `n − 1` first | | `noise` | `{"depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"}`, each a probability | | `options` | per method: `threshold`, `max_weight` and `ladder` (pauli), `max_bond` and `cutoff` (mps), `fusion` (statevector), `controls` (statevector and exact: `"standard"` or `"whole"`, a gate under many controls in standard gates or as one), `trajectories`, `memory_gb` | | `compile` | `{"epsilon", "level", "basis"}`: compile the program first (as `POST /v1/compile` does) and run the compiled circuit. With `basis` `clifford_t` (the default) it runs on the bit-exact method, and the result's `compiled` states its distance from the program | | `intent` | what the program must do: `{"distribution": {...}}`, `{"equivalent": ""}`, `{"prepares": ""}`, or a list. The result's `verdict` states the level the engine checked to (refused, parsed, executed, evidence, proof) and whether the intents hold, sealed by the receipt ([the verdict page](../wai-quantum/docs/verdicts.md)). A reference's check is held to the equivalence bound below | | `verdict` | `true`: the verdict with no intent | | `repair` | `true` or `{"max_iterations", "dyadic_bits", "sizes"}`: the program repaired first on the lines its diagnostics flag (as `POST /v1/repair` does); the result's `repaired` holds the program that ran | | `programs` | in place of `qasm`: a set, each item a program's text or a job (below) | | `sweep` | with `qasm`: a set over the program's inputs, `{"theta": [0.1, 0.2], "n": [2, 3]}` (below) | | `dry_run` | `true`: run nothing, and answer with the plan (below) | Counts are keyed by the program's classical bits, the highest bit first. With no measurement in the program, every qubit is measured into the bit of its own index. The program may use the standard gates, user `gate` definitions, and the control modifiers `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @` (on `x y z h s sdg t tdg p` and `cx cy cz ch cp ccx ccz`). Measurement and `reset` may come anywhere, and `if` acts on measured bits (`if (c[0]) x q[1];`, `if (c == 3) { … } else { … }`, OpenQASM 2's `if(c==1) x q;`). `for` loops over ranges and sets, and integer `const`s, are unrolled on reading. A program that declares an integer `input` is a generator: the service reads one circuit, so send it written out (`wai-quantum instance gen.qasm --input n=5`). `while`, `inv @` and `pow @` are refused, each with its line and what to write instead (`src/qasm.rs` lists everything read). ## Sets and sweeps One request can run many programs, as one job: - **A set:** `"programs"`, a list, each item a program's text or a job (`{"qasm", …}`). The request's other fields (`shots`, `observables`, `seed`, `intent`, …) apply to every item that does not set them itself. - **A sweep:** `"qasm"` with `"sweep"`, each of the program's inputs and its values. Every combination runs, the names in sorted order and the first varying slowest. An integer input (`input uint n;`) takes whole numbers, and the program is written out at each. A float or angle input (`input float theta;`, `input angle phi;`) takes any number, bound to exactly the double given. Every input the program declares needs values; a list of one fixes it. A set holds at most 100 items. They run one after another in one worker, inside one job's time limit, holding the memory of the largest item; each item's result is kept as text as it ends, so the results of earlier items take no more than they will as served. Each item is a plain job: its request is complete in itself, and it runs, plans and verifies as it would alone. A set with an item that does not plan is refused, naming the item. A job's result, or a set's with every item's request and result in it, may take at most `WAI_QSVC_RESULT_MIB` (64 MiB) as served. That is checked before anything runs, from a bound of the result read from the request: each item's counts list at most its shots (a `trajectories` run's, its trajectories), the outcomes its measured bits can give (a noiseless `stabilizer` run's, two to the power of the gates that branch, `h`, `sx` and `sxdg`: a GHZ state's two, however wide) and 65,536 outcomes, each its bit string, quotes, a colon, a comma and its count's digits; a verdict counts the critique's remarks the program can draw (one for each gate that does nothing and each pair that cancels or merges, not one for every gate), each intent's check with the 64 outcomes it lists at a time, and the reader's notes again; each other part is counted by what it may hold or at its length as RFC 8785 writes it, every number in its own form (`1e20` is 21 characters). A request whose bound passes the cap is refused at once, `422` and `J031`, with the bound, its largest part (the counts, the verdict, the reader's notes, the items' requests or the rest) and what makes that part smaller in the reason, and a dry run gives the bound as `result_bytes`. The cap is less than a set at its most can give: at about 22 bytes a 16-bit outcome, 100 points of 65,536 outcomes each are about 150 MB, and are refused; 100 points of 16 measured bits are taken, and run to the end, up to about 30,000 shots a point (30,878 without a verdict; 30,003 over MCP, whose runs ask for one). Send fewer points or shots, split the set, or run it on your own machine (`wai-quantum run`), where no cap applies. A pulse run is bounded the same way, from its schedule as read: its populations and its distribution by the qubits it simulates and the captures it reads, each keyed as wide as the program's bits, its counts by its shots, and each capture with its frame's and bit's names. The check as a result ends stays only as a backstop, for a job kept across a restart that lowered the cap. The result is a set (`"format": "wai.quantum.set/1"`). Each item has its own result and its own receipt. Each item's leaf is the digest its receipt signs (a failed item's: the digest of its request and its error). The head holds the Merkle root of the leaves, under the JWP Merkle domains, with the count, and the set's receipt seals the set's request and its head. Each item carries its `path` to the root, so one item can be shown sealed by its set without the others. `POST /v1/verify` checks the whole set: its receipt, every item as the request gives it, each item's receipt, the root, and each path. ```sh runnable needs=service curl -s https://wai.transaction.science/quantum-jobs/v1/run -H 'content-type: application/json' -d '{ "qasm": "OPENQASM 3.0; input float theta; qubit q; rx(theta) q;", "sweep": {"theta": [0, 0.5, 1]}, "observables": ["Z0"] }' ``` ## Dry runs `"dry_run": true`, on `POST /v1/jobs` or `POST /v1/run`, runs nothing. It answers `200` with the plan. A dry run counts as one submission, as a check does. | field | meaning | |---|---| | `method`, `why`, `qubits` | the method the job would run on, and why | | `memory_bytes`, `work` | the memory it would be admitted with, and the planner's work | | `time` | `seconds` with its `basis`: the median rate of the last jobs of the method this service timed, or, before it has timed three, the planner's nominal rate; or `seconds: null` with why, where the planner's work does not predict the time | | `energy` | `joules_micro_estimate`, class `Estimator`, from the median power of the last metered jobs of the method here; or `unestimated` with why. Never a reading | | `bounds` | the limits that apply: memory and time per job, jobs and memory per client, submissions per window, the queue, the most items in a set, the most a result may take (`result_bytes`), the most intents, the steps a request's programs may take to read, the largest body | | `result_bytes` | the most its result takes as served, bounded before it runs; past `bounds.result_bytes` it is refused, `422` and `J031` | | `client` | where you stand: active jobs, memory held, submissions left | | `accepted` | whether the service would take the job now, by every limit; if not, `status` and `refusal` (its message and code) say why | | `items` | for a set: each item's plan and estimates | ## Writing a program for a task A service started with a model (`WAI_QSVC_MODEL`, the model's directory) also writes programs. `POST /v1/write` takes a task, and the service's own model writes an OpenQASM 3 program for it. The engine checks each attempt against the task exactly as it checks a lesson's answer in the course. What went wrong goes back to the model, in the engine's words, for the next attempt, until a program passes or the rounds run out. The model is North Mini Code 1.0 (Apache-2.0), on WAI's own pure-Rust inference runtime ([wai-quantum-writer](../wai-quantum-writer/src/lib.rs)). ```json { "task": { "title": "A Bell pair", "task": "Entangle two qubits so that their measured bits always agree.", "intent": { "distribution": { "00": 0.5, "11": 0.5 } } }, "rounds": 4, "max_new": 3000 } ``` | field | meaning | |---|---| | `task` | an exercise in the course's form (`wai.quantum.exercise/1`): `title` and `task` in words (strings, or one per locale), an optional `starter` program, and what the program must do, as an `intent` (a job's intent) or as `cases` that each have one (a case without one takes the task's); optionally `constraints` (`allow`, `counts`, `t_count`, `two_qubit`, `no_smells`, `estimate`) and the `job` the check runs it as (`shots`, `seed`, `noise`) | | `rounds` | repairs after the first attempt: 0 to 4 (default 4) | | `max_new` | the most tokens one reply may take, its thinking included: 1 to 4000 (default 3000) | - **The answer** is `202` and the job, at once, since a write takes minutes. Its result is the transcript (`wai.quantum.write/1`). It names the task and its digest; the model by its name, licence and the digest of its files, how far its files were checked (`"checked": "blake3"`: the writer reads every byte against the model's manifest when it starts, and loads exactly the files the manifest lists), and the arithmetic it ran in (architecture, operating system, maths library, matmul kernel); the prompt by its version and digests; the decoding (greedy, with its thinking cap), the memory each check's run was planned in (`check_memory_bytes`) and the engine's version. It holds each attempt with its program, the program's digest and the engine's check, and says whether one passed and at what level. - **Sealed as every result is.** The service's receipt seals the transcript with the job's `request`, the writer's energy inside it, so `POST /v1/verify` checks it. How long each reply and each check took is the job's `timing`, beside the result and outside the signed body. - **Each attempt as it lands** is an `attempt` event (`{"id", "attempt", "timing"}`) on `GET /v1/jobs/{id}/events`. A running write's job also gives `progress`: how many attempts the engine has checked, and the latest. - **The same task gives the same transcript, in the same arithmetic.** Decoding is greedy, and the task reaches the model in its canonical form (the form its digest is taken over), so the same model files, task, options and engine give the same bytes on a host whose arithmetic is the same: the transcript's model names it. On another architecture or maths library the logits can differ in their low bits, and a near tie can go the other way. A write that was running when the service stopped is run again, once, as a job is, and gives the same transcript. - **One write at a time.** The model runs in one writer process (the binary's `writer` mode), started with the service and kept for every write, so the model loads once. Writes wait in the queue in their order, beside the circuit jobs, and neither waits for the other: each kind fills the queue up to `WAI_QSVC_MAX_QUEUE` alone. A client holds one queued or running write, however many it sends at once: a second is refused (`429`, `J030`). - **Its limits.** A write may run for `WAI_QSVC_WRITE_SECS` (an hour by default). The writer's resident memory, its model's mapped pages among it, is held to `WAI_QSVC_WRITER_GB` (96 GiB). The engine's checks inside it plan within one job's memory (`WAI_QSVC_JOB_GB`), so the check refuses a program larger than a job may be, as a job's plan does. A writer that passes its memory or its time, or whose write is cancelled, is stopped: the write fails saying which, and the next write starts the writer again. A write cancelled while the writer starts is never sent to it. - **What a task may hold.** A task is held to the course's form before it is queued: every bound whole numbers, every constraint one the course names, a job of `shots`, `seed` and `noise` alone, at most 16 cases and 8 intents in one place. Its canonical form is at most 64 KiB, what the model is first told of it at most 8 KiB, and a program an intent compares with is held to the equivalence bounds a job's intent is (`422`, `J031`). What costs nothing to read (the shape, the options, the sizes, the number of cases and intents) is refused first, and so is what refuses whatever is sent (a write the client already holds, its results not yet read, a full queue of writes, a spent window), uncounted; then the write waits for a turn to be read, as a job does, and counts against its client's window, whatever comes of it; only then are its programs read, each once and all within one budget of 1 000 000 statements, in a reading child whose memory and time are capped (`WAI_QSVC_READ_GB`, `WAI_QSVC_READ_SECS`), as a job's are. Every case gives the same inputs, or none does. - **Refusals.** A service with no model answers `503` (`J080`). `GET /v1` says under `write` whether this one has a model, and names it (name, licence, digest) once it is loaded. A task without an intent, or with a case that has none and no task's intent to take, or not in the course's form or past its sizes, is `400` (`J081`). Options out of range, or a member a write does not take, are `400` (`J082`). An `Idempotency-Key` works as it does for a job. The MCP tool `write_circuit` takes the same and runs as a task ([For agents](#for-agents)). `WAI_QSVC_WRITER_SCRIPT` names a script the writer runs in the model's place: a JSON list of replies, each a string, or null for a reply that never left its thinking. The service's tests write with one. `smoke.sh` writes a Bell pair's program when `SMOKE_WRITE=1`, against a service with a model. ## wasmQ envelopes and qudit jobs Two kinds of job besides a circuit's run as every job runs: admitted in the order of what each step costs, read and planned in a reading child, queued within the client's and the service's limits, run in a worker whose memory and time are watched, kept in the store across a restart, and sealed with the service's key. Each route waits as `POST /v1/run` does: the job once it has ended (`200`), or the job to poll (`202`, `GET /v1/jobs/{id}`). - **`POST /v1/wasmq`**: a wasmQ envelope ([the profile](../extensions/quantum-wasmq.md), its first and second versions), an OpenQASM 3 program and the WebAssembly modules its `extern`s call between gates. The body is the envelope's bytes, or JSON: `{"envelope_base64"}`, or `{"example"}`, one of the profile's own envelopes (`GET /v1/wasmq/examples` lists them). The envelope is opened and held to the profile in the reading child (a refusal keeps its code, `W001` to `W017`), and its run is planned from the envelope alone: its work in the profile's operations (§4.8) and an upper estimate of its memory. It runs in a worker on the native engine, each callout a metered call, the same bytes on every host and engine; a fault the envelope's own budgets cause is sealed, as the profile seals it. The job's `request` is what the receipt binds, the contract and the envelope's digest, and the envelope is kept in the store under that digest. A second-version run's `energy.run` is the worker's figure, read as every job's is below, or the unmetered form with the reason; each callout's is unmetered, as no meter resolves one call. - **`POST /v1/qudit`**: a qudit job ([quantum-sim §9](../extensions/quantum-sim.md)): `{"d", "qudits", "gates"}` with `shots`, `seed` and `amplitudes`, a register of 2, 3, 4 or 6 levels run in whole numbers at 2^-30. Its bounds are checked in the reading child (`J090`, `J091`, `J092`), and its result carries the worker's energy, as a job's does. The service's own limits refuse a run and say so; none cuts one short. An envelope past 786,432 bytes (its base64 fits a request's body; the profile's own limit is 128 MiB), or whose run asks more than 2^38 of the profile's operations (a sixteenth of its 2^42, about eleven minutes, within a job's time), is refused before it is queued (`422`, `J031`, naming `wai-quantum run ENVELOPE.wai`); one whose memory estimate passes a job's is refused as any job is. A worker past its admitted memory or the job's time is stopped, and the job fails saying which, with nothing sealed. ```sh runnable needs=service # The profile's surface-code round, decoded in the middle of the circuit by its reference decoder. curl -s https://wai.transaction.science/quantum-jobs/v1/wasmq -H 'content-type: application/json' -d '{"example": "surface3-decode64"}' # A qutrit Bell pair. curl -s https://wai.transaction.science/quantum-jobs/v1/qudit -H 'content-type: application/json' \ -d '{"d": 3, "qudits": 2, "gates": [{"gate": "fourier", "qudits": [0]}, {"gate": "csum", "qudits": [0, 1]}], "shots": 1000}' ``` ```sh norun=data # An envelope of your own, as `wai-quantum pack-wasmq` writes it. curl -s https://wai.transaction.science/quantum-jobs/v1/wasmq -H 'content-type: application/octet-stream' --data-binary @round.wai ``` `POST /v1/verify` checks either job as it checks every job, a wasmQ job held to the profile's form (§6.5). `wai-quantum verify --bundle ENVELOPE.wai` runs the envelope again and compares, and `wai-quantum qudit check` runs a qudit job's request again, bit for bit. The MCP tools `run_wasmq` and `run_qudit` run the same jobs ([For agents](#for-agents)). ## Methods | method | takes | answers | accuracy | |---|---|---|---| | `exact` | angles that are dyadic multiples of 2π, up to 26 qubits | counts, expectations, amplitudes, a state hash | bit-exact | | `statevector` | any circuit, up to 32 qubits | counts, expectations, amplitudes | exact | | `statevector_single` | any circuit with gates on at most four qubits, up to 33 qubits, where the host has a GPU | counts, expectations, amplitudes | single | | `stabilizer` | Clifford circuits (rotations by whole quarter turns), up to 32768 qubits, optionally with depolarizing, dephasing and readout noise | counts, expectations | exact; with noise, an estimate | | `pauli` | any circuit, up to 1024 qubits | expectations | bounded | | `mps` | any circuit, up to 4096 qubits, at bond dimension up to 256; best when entanglement stays low. Named, a job past one job's work at the bond it asks for runs under a meter of the bonds it reaches (`metered` in its plan, whose `work` is then what the meter allows) and stops with `J011` if it passes it | counts, expectations, amplitudes | exact when nothing is truncated, otherwise heuristic | | `tensor_network` | any circuit | amplitudes | exact | | `cutrank` | a Clifford circuit, then one layer of one-qubit gates at any angles, up to 4096 qubits, as wide as the state's cut-rank allows | counts, amplitudes | exact; single when only a single-precision table fits | | `cutrank_single` | as `cutrank`, with the table on the host's GPU, where it has one; only when asked for | counts, amplitudes | single | | `stabilizer_rank` | Clifford+T circuits, up to 32768 qubits, as many T gates as one job's sum of `2^t` (an expectation value's `4^t`) branches allows | amplitudes, expectations | exact; with `terms` or `epsilon`, an estimate with its bound and the probability it holds | | `density_matrix` | noisy circuits, up to 16 qubits within a job's memory | counts, expectations | exact | | `trajectories` | noisy circuits, up to 32 qubits | counts, expectations | estimate | How the methods run: - **statevector** groups consecutive gates into passes over the state. Each pass applies its gates tile by tile, in a buffer that fits a core's cache. The grouping never changes a bit of the result. - **cutrank** takes a Clifford circuit followed, on each qubit, by one-qubit gates at any angles (a qubit's gates from its first non-Clifford one on act on that qubit alone). - It writes the Clifford part's state as a graph state with local Cliffords, and sums the amplitude one qubit at a time along an order of the qubits, in a table of `2^cut` entries. Here `cut` is the state's cut-rank, its entanglement in bits, across the cut the sum has reached. - The planner makes one short search for an order with narrower cuts, enough to bound the job's memory and work. The run searches longer, from several seeds on threads of their own, and keeps the cheapest order that is no wider than the planner's. The cost is set by the widest cut, `max_cut` in the result, not by the number of qubits. - Counts are exact samples, drawn qubit by qubit from the table's conditional probabilities. - Amplitudes carry the circuit's own global phase. A stabilizer tableau does not fix it, so one amplitude of the Clifford part is tracked exactly, gate by gate, and the summed amplitudes are scaled to agree with it, together with the phase the lowering to Clifford gates drops. A circuit with so many Hadamards that the tracking would take more than about a minute reports `global_phase` as not fixed instead. - The table is held in double precision when it fits the job's memory, and in single precision otherwise (accuracy class `single`). - **pauli** reports each value with its truncation `bound`. It holds at most as many Pauli terms as the job's memory allows; a sum that would outgrow it raises its threshold until it fits (`threshold_reached`), and the bound covers everything that dropped. Unless `ladder` is `false`, it also reports a `ladder`: the value and bound at up to three thresholds a decade above the one asked for, so you can see the value settle as the threshold drops. - **stabilizer**, with noise, samples by Pauli frames against one noiseless outcome of the tableau, 64 shots to a machine word. Its counts have the law of independent noisy shots. Its expectation values are estimates with standard errors, from at least 10,000 shots unless `trajectories` sets the number. Amplitude damping is not a Pauli channel and is refused. - **trajectories** treats every shot as an independent noisy trajectory in law. Shots that took the same errors share one state, so the error-free part of a circuit, and the error-free majority of the shots, run once. `histories` in the result counts the distinct error histories that ran. ### Under a contract A job may carry a `contract`: the class wanted, a deadline, a cost ceiling, an energy ceiling, and whether a provider may be used ([section 8.7](../extensions/quantum-providers.md) of the provider protocol). The service prices each method here for it, rejects those that cannot meet it, runs the cheapest of the rest and seals its reasoning in the receipt. A job no method here meets is refused at once, and in a dry run, with every method's reason (`J071`). The service reaches no provider: none is among its routes, whatever the contract allows, and a contract that wants a device's counts is refused. A dry run's energy is an estimate from what it has metered, and it promises nothing. A `max_joules` is held by a live reading of the worker while it runs, about every ten milliseconds as the host schedules the service: at the ceiling the worker is killed, read whole before it is reaped where the host allows, and a receipt is sealed that says so, with no answer in it, the worker's whole figure and the longest time between two reads. A Mac whose counter reads nothing for a process (an Intel one) reads no worker, so its routes are refused under a ceiling. A route on a host or a device nothing here reads is refused under a ceiling (`energy_unwatched`), and an estimate clearly past the ceiling refuses a job before it runs (`energy`). A run past its contract's deadline is stopped (`J074`), and nothing is sealed. A queued job keeps the reasoning it was admitted with across a restart. A contract is one job's: a set refuses one (`J073`). ### Programs that measure in the middle A program that measures a qubit and then acts on it, resets a qubit after its first gate, or uses `if` on measured bits runs on `statevector` or `stabilizer`. `auto` picks the stabilizer tableau when every gate is Clifford, and the state vector otherwise. - **Counts** split their shots. The shots enter together, and at each measurement or reset they divide between the outcomes as independent shots would: binomially, in the outcome's probability. Each outcome then runs on in its own copy of the state. Shots that read the same outcomes share one state, and the counts have the law of independent runs. - **Expectation values** are exact: the mixture over every branch of non-zero probability, at most 4096 branches. As for any program, they describe the state before the final measurements. A measurement whose qubit nothing touches afterwards, and whose bit no later condition reads, is not made for them. - The result's `dynamic` member reports what the walks did: - `measurements_and_resets`; - `branches_run` and `states_at_once` for the counts; - `branches_weighed` for the expectation values. Noise in such a program, and amplitudes of one, are refused, each with its reason. The state vector holds a copy for each branch still running, so it takes one qubit fewer than for a program that measures only at the end. ```sh runnable needs=service # Teleportation: ⟨Z2⟩ = cos 0.7 and ⟨X2⟩ = sin 0.7, exactly. curl -s https://wai.transaction.science/quantum-jobs/v1/run -H 'content-type: application/json' -d '{ "qasm": "OPENQASM 3.0; qubit[3] q; bit[3] c; ry(0.7) q[0]; h q[1]; cx q[1], q[2]; cx q[0], q[1]; h q[0]; c[0] = measure q[0]; c[1] = measure q[1]; if (c[1]) x q[2]; if (c[0]) z q[2]; c[2] = measure q[2];", "observables": ["Z2", "X2"], "shots": 1000 }' ``` `auto` picks the first method that takes the job. With noise, it tries `density_matrix`, then `stabilizer`, then `trajectories`. For counts, it tries `stabilizer`, `exact`, `statevector`, `statevector_single`, `cutrank`, then `mps`. For expectations alone, it tries `stabilizer`, `exact`, `statevector`, `statevector_single`, `stabilizer_rank`, `pauli`, then `mps`. For amplitudes alone, it tries `exact`, `statevector`, `statevector_single`, `cutrank`, `stabilizer_rank`, `tensor_network`, then `mps`. The job's `why` names the method chosen and the ones that could not take it. The accuracy classes: - **bit-exact**: the same bits on every machine. Amplitudes are held in 30-bit fixed point, and every output is an exact function of them. - **exact**: no approximation beyond double-precision rounding. - **single**: no approximation beyond single-precision rounding, about 10⁻⁷ relative per gate; sums are taken in double precision. - **bounded**: an approximation, with a rigorous bound on the error (`bound`) beside each value. - **estimate**: a statistical estimate, with its standard error (`stderr`). - **heuristic**: an approximation, reported with the standard estimate of its error but not a bound. A matrix product state that truncated reports its discarded weight and its fidelity estimate, and a `warning` when the estimate is below 0.9. Counts are always seeded samples. ## Endpoints | | | |---|---| | `GET /v1` | the service: its methods, limits, load and receipt key | | `POST /v1/run` | submit a job and wait up to 25 s: the finished job, or `202` and the job if it is still running | | `POST /v1/jobs` | submit a job or a set: `202` and the job, with its `id`, once it is in the store; with `dry_run`, `200` and the plan | | `GET /v1/jobs/{id}` | the job, as RFC 8785 text, the same on every route and after a restart: its status, the request as sealed (the canonical form of the one submitted), its result when it is done, and the digests it is kept under; `?wait=S` (at most 25) holds the answer until its status changes | | `GET /v1/jobs/{id}/events` | the job's status as server-sent events: a `status` event at each change, for a write an `attempt` event for each attempt as it lands, then one `job` event with the whole job when it ends | | `DELETE /v1/jobs/{id}` | cancel a queued or running job | | `POST /v1/verify` | `{"request": ..., "result": ...}`: whether the result's receipt seals that request and that result and every rule of [the provider protocol's section 8.4](../extensions/quantum-providers.md#84-what-a-verifier-checks) holds, read from the body's own text: `valid`, `reasons`, `key`, `form`, and what a provider's result says of itself. A body that does not read as a job's, is no job, has no receipt, or is longer than the 4 MiB and 2^19 values it reads (`too_large`, stating both) gets that reason as its verdict; past them, `wai-quantum verify` reads further. It is counted against the client's window for checks before it is read, and read in a reading child on one of the client's check turns (a body past 1 MiB on one of four large bodies read at once, one a client); an estimate or an algorithm's answer whose request runs again nowhere is not valid, and says why (`rerun_error`). An estimate or an algorithm's answer whose receipt holds is also run again, counted and read as a check is | | `POST /v1/critique` | `{"qasm"}`: the program's measures (gates by kind, two-qubit gates, depth, T count, whether it is Clifford, a modelled fidelity) and its smells, each with its lines | | `POST /v1/equivalent` | `{"a", "b"}`: whether two programs are the same circuit up to a global phase, and how that was decided | | `POST /v1/convert` | `{"to", "program" \| "program_base64", "from"?, "profile"?, "check"?}`: the program carried between OpenQASM 3, QIR and jeff (as base64), with the program as OpenQASM 3, each note with its code, what was read and written, and the check (the output read back and held to the program by the equivalence ladder); a refusal is the reader's, with its code and place ([the formats page](../wai-quantum/docs/formats.md)) | | `POST /v1/improve` | `{"qasm"}`: the program with its smells fixed, each candidate with its measures and checked the same circuit | | `POST /v1/shortcuts` | `{"qasm"}`: each exact classical route to simulating the program, priced on it, and the cheapest for each question | | `POST /v1/compile` | `{"qasm", "epsilon", "level", "basis"}`: the program compiled through checked passes (to Clifford+T within `epsilon`, by default `1e-6`), with a certificate of every pass and the distance, sealed with the service's key in a job's form so `POST /v1/verify` checks it ([the compiler page](../wai-quantum/docs/compiler.md)) | | `GET /v1/targets` | the built-in device targets (synthetic), each with its qubits, pairs, gates, errors and hash | | `POST /v1/estimate` | `{"qasm"}` (or `{"counts"}`, `{"routine"}`), with `budget`, `qubit`, `calibration`, `max_slowdown`, `clock` and `export`, each optional: what a fault-tolerant machine would need to run the program, its logical counts, the estimate and the frontier, each estimate sealed with the service's key in the resource-estimate receipt family and the answer in a job's form; `POST /v1/verify` checks the seals and re-runs it ([the estimate page](../wai-quantum/docs/estimate.md)) | | `POST /v1/repair` | `{"qasm", "proposed"?, "max_iterations"?, "dyadic_bits"?, "sizes"?}`: the program repaired on the lines its diagnostics flag alone, a proposed repair taken only if it changes no other line, and what needs a person | | `GET /.well-known/wai-quantum-receipt-keys` | every key receipts are signed with, each with its validity window, a retired one's transition to its successor signed by both | | `GET /metrics` | the service's counters in the text exposition format ([wai-metrics](../wai-metrics/README.md)), for the operator at the service's own address: `wai_quantum_requests_total{route,method,code}` (the route a path matched, never the path), `wai_quantum_jobs{status}` (the jobs held now, every status from the start), `wai_quantum_memory_held_bytes`, `wai_quantum_restored_jobs{outcome}` (what the store held at the start), `wai_quantum_lent_turns_total{pool,event}` (the turns lent to a client past its share, in the `reading` pool and the `checks`: `lent`, and of the calls on one, `moved` onto the client's own turn as that came free, `kept` on the lent turn past its five seconds as no one waited, and `taken_back` for those who waited; see *Turns* below) and `wai_quantum_energy_unmetered` (1 where no meter backs the jobs' energy). It is answered before the API key and the client's limits; `HEAD` as `GET` without the text, another method `405`, and a request carrying a field a front sets (`Forwarded`, `Via`, `X-Forwarded-*`, `X-Real-IP` and the like) `404`, as a path the service does not have. No job, client, key or address is a label. It is outside the OpenAPI document and the MCP endpoint | | `POST /v1/qec/code` | `{"code", "certify"?, "bound"?, "certificate"?}`: an error-correcting code (`surface:D`, `toric:L`, `steane`, `color:D`, `bb:72` to `bb:288`, `gross`), its checks, `k`, layout and decoders, and its distance certificate, or a given certificate checked against it | | `POST /v1/qec/decode` | a code's `syndrome` or `error` decoded at code capacity, or a `memory` experiment's, a `circuit`'s or a `model`'s fired `detectors` decoded (or its `shots` sampled and decoded), with `decoder` | | `POST /v1/qec/threshold` | `{"family", "distances", "p", "shots", "decoder"?}`: logical failure rates with their standard errors, the code model fitted at circuit level, and whether each larger distance failed less | | `POST /v1/pulse/schedule` | `{"qasm", "pulse"?}`: a pulse program's schedule on frames, every play, delay, phase and frequency change, capture and barrier at its sample, each waveform's samples, the gate calls, and the digests a pulse run's receipt binds | | `POST /v1/pulse/check` | `{"request", "result"}`: a pulse job checked by running it again here: its receipt, its program's digest, and its result held to the run field by field | | `POST /mcp` | the Model Context Protocol endpoint (streamable HTTP, answered as JSON, no key): the tools, resources and prompts below | The three error-correction calls answer in a job's form, sealed with the service's key, so `POST /v1/verify` checks them ([the error-correction page](../wai-quantum/docs/qec.md)). So do the two pulse calls; a pulse run itself is a job of the method `pulse` (`"method": "pulse"`, its options under `"pulse"`), queued, run and sealed as every job is ([the pulse page](../wai-quantum/docs/pulse.md)). A job may also give `mitigate` beside `noise` and `observables` (`"zne"`, `"pea"`, `"pec"`, `"cdr"`, `"vd"`, `"readout"`, or `{"method", ...}` with its options): its result then carries each value raw and mitigated with its error bar, the method and its assumptions, inside the receipt. | `POST /v1/gradient` | `{"qasm", "params", "hamiltonian", "gradient"?}`: a Hamiltonian's expectation at the program's parameters and its exact gradient by each (`adjoint` or `parameter_shift`), in f64 with each component's rounding bound | | `POST /v1/optimize` | `{"qasm", "params", "hamiltonian", "optimizer"?, "steps"?, "step_size"?, "gradient"?, "seed"?}`: the expectation minimised by gradient descent, Adam or SPSA, every step a link of a digest chain from the request | | `POST /v1/vqe` | `{"hamiltonian", "qasm" and "params" or "ansatz": {"layers"}, …}`: the variational eigensolver, beside the exact ground energy by diagonalisation | | `POST /v1/qaoa` | `{"graph": {"nodes", "edges"}, "layers"?, …}`: Max-Cut by QAOA, beside the maximum cut by brute force | | `POST /v1/build` | `{"algorithm": "qpe" \| "amplitude_estimation" \| "adder" \| "qsvt", …}`: a standard algorithm's program, how to read it, and the equivalence ladder's verdict against a reference built from its definition | | `POST /v1/write` | `{"task", "rounds"?, "max_new"?}`: a program written for the task by the service's model, each attempt checked by the engine; `202` and the job, its result the signed transcript ([above](#writing-a-program-for-a-task)) | | `POST /v1/wasmq` | a wasmQ envelope run as a job: its bytes, or `{"envelope_base64"}` or `{"example"}` ([above](#wasmq-envelopes-and-qudit-jobs)) | | `GET /v1/wasmq/examples` | the profile's own envelopes a request may name: each one's name, profile, what it shows, size and digest | | `POST /v1/qudit` | a qudit job run as a job: `{"d", "qudits", "gates", "shots"?, "seed"?, "amplitudes"?}` ([above](#wasmq-envelopes-and-qudit-jobs)) | | `POST /v1/check` | the sign-off loop's check (`{"qasm", "inputs"?, "intent" \| "task", "shots"?, "seed"?}`, as `check_quantum_program` takes it): whether the program reads, its diagnostics, measures and smells, the fixes the engine would make on its flagged lines, and given an intent or a task, each case graded `passed`, `failed` or `unchecked` by the strongest layer the engine has, with what went wrong and `next`. Nothing is sealed ([below](#the-sign-off-loop)) | | `POST /v1/grade` | `{"grade": {"task", "program"}}`: the program graded against the task's cases ([the provider protocol's section 8.10](../extensions/quantum-providers.md#810-a-grade)), sealed with the service's key with the task and the program, so `POST /v1/verify` makes the grade again | | `POST /v1/attest` | `{"job", "intent"?, "trust"?, "at_ms"?, "skew_ms"?}`: a job another host sealed, verified as `POST /v1/verify` verifies it and held to the trust list and the time given (§8.8), then signed off with the service's key (§8.9); `attested`, beside the sign-off and outside its seal, says what the job was checked against. A job that is not valid is refused (`422`, `J001`) and nothing is sealed | | `POST /mcp` | the Model Context Protocol endpoint (streamable HTTP, answered as JSON, no key): the tools, resources and prompts below | ### The sign-off loop ```sh runnable needs=service # A Bell pair graded against its distribution; the grade sealed here, and worked out again. curl -s https://wai.transaction.science/quantum-jobs/v1/grade -H 'content-type: application/json' -d '{ "grade": { "task": { "title": "A Bell pair", "intent": { "distribution": { "00": 0.5, "11": 0.5 } } }, "program": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n" } }' > grade.json wai-quantum verify grade.json ``` `POST /v1/check`, `/v1/grade` and `/v1/attest`, and the MCP tools `check_quantum_program`, `grade_quantum_program` and `attest_quantum_job`, are the engine's own (`wai_quantum::engine::front`), as the command line's `check`, `grade` and `attest` and its server's tools are: the same arguments give the same answer, a grade and a sign-off sealed here with the service's key (and the time, as every receipt here), their energy unmetered with the reason. Each is counted as a check is, held to its bounds in a reading child, and made there: - a check of a task, and a grade, hold each case to the bounds before any case runs: the task's job is read as a job's is, and each case's intent that compares the program with a reference is held to the equivalence check's bound with the program written out at that case's inputs, so a generator's narrow cases pass and a wide one is refused at that case (`422`, `J031`, `case N: …`, naming `wai-quantum grade`). Each case then runs within half the reading child's memory (a case past it is `unchecked`, and says why); - an attestation holds what verifying the job works out again (a verdict it seals, a grade case by case; an estimate or an algorithm's answer, run again within its own route's bounds) and the intent its sign-off checks to the same bounds, and is refused past them, naming `wai-quantum attest`. Its body is at most the 1 MiB a request may be: a larger job is attested on your own machine; - a sealed grade sent to `POST /v1/verify` or `verify_quantum_receipt` is worked out again within the same bounds, case by case; past them its receipt is still judged, and its verdict read as the host's statement, with why. The three error-correction calls and the five of the variational and algorithm library answer in a job's form, sealed with the service's key, so `POST /v1/verify` checks them ([the error-correction page](../wai-quantum/docs/qec.md), [the variational page](../wai-quantum/docs/variational.md)). A job's `params` binds its program's parameters (`input float theta;`, `input angle phi;`) exactly. A job may also give `mitigate` beside `noise` and `observables` (`"zne"`, `"pea"`, `"pec"`, `"cdr"`, `"vd"`, `"readout"`, or `{"method", ...}` with its options): its result then carries each value raw and mitigated with its error bar, the method and its assumptions, inside the receipt. A job may give a `target`: a built-in target's name, or a target document (`wai.quantum.target/1`, its schema in `GET /v1/openapi.json`). The program is compiled to the target's gates, placed on the qubits and routes most likely to succeed, and simulated under the noise its calibration gives. The result's `target` names the layout, its expected success, the noise and the target's hash, inside the receipt. The service reads the document it is given and fetches nothing. Every refusal says why and ends with its code, which the body also carries as `code` ([the job layer's codes](../wai-quantum/docs/job-refusals.md), beside the reader's): `400` for a program or job that does not read (a set that does not read: `J007`), `422` for one larger than a job may be (`J031`), `429` past the per-client limits (`J030`), `503` when the queue is full or no turn to read it came (`J032`), on a private deployment `401` without one of its API keys (`J033`), an `Idempotency-Key` that does not read (`400`) or was sent before with another request (`422`): `J034`; and for a write, `503` on a service with no model (`J080`), `400` for a task that does not read (`J081`) or options out of range (`J082`). `GET /v1` states the limits. ## Status and limits A job is `queued`, `running`, `done`, `failed`, `cancelled`, or `interrupted`: the service stopped while it ran, after it had run it again once, so it has no result, and `error` says so. Follow a job by its event stream (`GET /v1/jobs/{id}/events`), or by polling `GET /v1/jobs/{id}`, with `?wait=S` to hold each poll until the status changes. A client holds at most `WAI_QSVC_CLIENT_STREAMS` (4) long polls and event streams at once: a long poll past them is not held, and says so, `429` (`503` past everyone's), `J032`, with `Retry-After: 2` and the job's `status`; wait that long before polling again, or poll without `?wait`. Every answer carries the client's submission window in the rate-limit fields of the IETF HTTP API working group: `RateLimit-Policy: "submissions";q=30;w=600` (at most 30 submissions in 600 s) and `RateLimit: "submissions";r=12;t=34` (12 left, one more in 34 s). A refusal past the limits carries `Retry-After`. A submission is admitted in the order of what each step costs: 1. A job sent again under its `Idempotency-Key` is answered with the key's job, before any limit: it reads nothing, takes no turn and counts as nothing. A job given in another format (`"source"`) is matched as it was received, so it too comes back past the window. Sent again in some other form, it is read and counted to tell whether it is the same request. 2. What reads nothing and refuses whatever is sent: your jobs at once (`429`), the memory they hold (`429`), a full queue (`503`), a spent window (`429`). None of these is counted, so waiting as told spends nothing. 3. It waits for a turn to be read. If none comes, it is refused (`503`, `J032`): nothing was read and nothing is counted. 4. With its turn the submission is counted against your window. Every refusal after this counts. 5. What reads nothing: every whole number exact, at most 16 intents (each of a set's items too), at most 100 items in a set. 6. Its programs are read: the job's own, a set's items and a sweep's points, and every intent's reference, each once however often admission reads it, and all of them within one budget of 22,000,000 steps (statements the reader runs, gates it writes, a generator's loop passes and what it writes out, and every 128 bytes of a value it makes or copies). That is as many as one program may take, its statements and its gates each to the reader's bound, so a job whose own program reads within the reader's bounds reads here. A request whose programs take more is refused, `422` and `J031`, with the command that runs it on your machine. 7. The job is kept and queued, within your and the queue's limits, and within the most a result may take: one whose result's bound passes `WAI_QSVC_RESULT_MIB` is refused, `422` and `J031`, before it runs. `Retry-After` is the pace at which a refusal that counts would not spend your window (the window over its submissions, at least 5 s), or, past the window, when the next submission opens. A `503` carries it too. Reading happens outside the service's own process, as each job's run does: each request is read by a child, this binary run as `wai-quantum-service read`, sandboxed as a worker is. - **Turns.** Four requests are read at a time (jobs, dry runs and the MCP tools' verdicts), and four checks beside them. A client has its share of each: `WAI_QSVC_CLIENT_READS` (2) of the first and `WAI_QSVC_CLIENT_CHECKS` (1) of the checks, so no client holds a whole pool. Past its share, one more turn is lent to a client while the pool keeps two free for others, so its quick calls (a critique, a lint, an estimate) pass its own slow one: of the checks, at their share of one; the reading pool's four turns, at its share of two, leave none to lend. A call on a lent turn moves onto the client's own turn as soon as one comes free, and then has the whole of its time and gives the lent turn back, so the client's quick calls pass it on that one; until then it runs past five seconds only while no other client's request waits for a turn and none of its own client's has waited seven seconds, and once one has it is refused, `503` and `J032`, to send again: two long calls of one client never hold its quick ones past their wait. Its other requests wait behind its own turns, in order, so an agent's burst of parallel calls is answered one after another: up to eight of them may wait (so its eleventh request at once, of readings or of checks, is refused), at most 32 requests wait in all, each for ten seconds, and past any of these a request is refused, `503` and `J032`, having read and spent nothing, saying whether its own requests or the pool held it up. A client is its address, so people behind one address (an office, a carrier's NAT, a class) share one client's turns and its window: raise the shares, or give such a group an API key of its own on a private deployment. - **Memory.** A child's memory is polled every 50 ms from a thread of its own, and the child is killed once it holds more than `WAI_QSVC_READ_GB` (4); the request is refused, `422` and `J031`. The kill is a signal the service sends itself (`SIGKILL`, to the child's process group, and again every 100 ms until it has ended), so it needs no new process. On macOS the poll reads the child's physical footprint, which counts its compressed and swapped pages; on Linux its resident and swapped pages. A poll is not a kernel limit: a child can pass the cap by what it touches in one interval before it is killed. On Linux the child also holds its own address space to twice the cap and a gigabyte, so an allocation past that fails at once (`422`, `J031`), and the kernel ends a reader or a worker before the service when the host runs short. macOS gives an unprivileged process neither, and has no limit that counts compressed memory. - **Time.** A child that reads for longer than `WAI_QSVC_READ_SECS` (45) is killed and the request refused, `422` and `J031`. Every request is answered within `WAI_QSVC_ANSWER_SECS` (55) of its arrival, under the 60 s a proxy in front commonly waits for an answer: its waits for a turn and its readings end by then, `POST /v1/run` and the MCP run tools wait for their job only for what is left and then answer with the job, still running, to poll, and an MCP answer's verdict is read again only if time is left (its verdict says so where it was not). An error-correction call's shots are measured as they run: each point is set up and sampled, and a pilot of its own shots decoded, before the rest of any; it is refused at once when what is measured says it would take more than `WAI_QSVC_QEC_SECS` (25), and stopped if it runs a fifth past that. - **Its answer.** What a reading gives back comes into the service's memory, which every job shares: one that would take more than `WAI_QSVC_READ_ANSWER_MIB` (128) once read, about 80 bytes a value, is refused in the child before it is written (`422`, `J031`), and the service reads it off its request threads. A dry run gets back the plans and points alone. A check's answer can be that large: a critique of a program of 200,000 gates that each draw a remark (`qubit q; for int i in [1:200000] { x q; }`) is refused so; one of 150,000 is answered. - **What ended jobs keep.** A job keeps its request as the canonical text the store keeps, and a set lets go of its items once it ends. What ended jobs keep in memory, their requests' text and their results as RFC 8785 text, is counted in two parts. Results that have been read (or ended more than `WAI_QSVC_KEPT_MIN_SECS`, 600 s, ago) are held to `WAI_QSVC_CLIENT_KEPT_MIB` (128) a client, its oldest dropped first, and `WAI_QSVC_KEPT_MIB` (4096) in all, those of the client keeping the most dropped first (between two keeping as much, the newer); only these are ever dropped before `WAI_QSVC_RETAIN_SECS`. Results not yet read are dropped by neither cap: a client's next job is refused, `429`, while its unread results, with each job it has running at its result's bound and the new one at `WAI_QSVC_RESULT_MIB` (64), would pass `WAI_QSVC_CLIENT_UNREAD_MIB` (256), and every next job is refused, `503`, while all unread results, with every job queued or running at its result's bound and the new one, would pass `WAI_QSVC_UNREAD_MIB` (8192). A job whose result's bound passes `WAI_QSVC_RESULT_MIB` is refused before it runs (see [Sets and sweeps](#sets-and-sweeps)). The bounds that remain: memory for ended jobs stays under the two caps, 12 GiB at the defaults, the results of jobs still queued or running counted within the unread cap at their bounds; reaching the unread cap takes 32 clients each holding its whole allowance unread, and holds every next job for at most 600 s; a client is its address, so people behind one address share its allowances, and one of them who leaves 256 MiB unread holds the others' next jobs until it is read or 600 s pass; and an honest client's read result is dropped early only where it, of all clients, keeps the most. A request's body is parsed only once its client's window has room (or, for a resend under a key the client holds a job under, first, as it is answered before any limit). A receipt check has a window of its own, `WAI_QSVC_CLIENT_VERIFIES` (120), asked before its body is read. An MCP message over 64 KiB from a client past its window is read only for its id, method, tool and key, and answered under its own id with a tool result that says why; a resend under a key the client holds, and a receipt check, are read whole. A job's report is one RFC 8785 text on every route (a poll, its events, a cancel, and in the MCP job tools' answers, written with RFC 8785's numbers, the job's own bytes, with each JSON-RPC id echoed as the text it was sent as: `1.0`, `1e3` and an integer past 64 bits as written) and after a restart: its request and its result are written in as the text each is kept as, never parsed again, and the memory a large request or answer was parsed into is given back to the system once it is answered. On macOS the service starts itself with the allocator's large-block cache off (`MallocLargeCache=0`): that cache kept every large block a parsed request had freed, dirty and charged to the service (on arm64, 30 requests of a megabyte kept 1 GiB; with it off, 2 MiB). - **Ending.** A child that panics, overflows its stack or aborts is answered `500`, and the service runs on. A client that hangs up ends its child. - **Connections.** A request whose header section takes longer than `WAI_QSVC_HEADER_SECS` (150) to arrive is closed; the same limit closes a connection left idle between requests, so it is set above a proxy's own idle limit (two minutes, commonly), and the proxy, not the service, closes its idle connections. A body that takes longer than `WAI_QSVC_BODY_SECS` (30) is refused, `408`. A client may have `WAI_QSVC_CLIENT_REQUESTS` (32) requests open at once; past it a request is refused, `503`, before its body is read (a health check is answered whatever that is). Long polls and event streams are held to `WAI_QSVC_CLIENT_STREAMS` (4) a client and `WAI_QSVC_STREAMS` (512, at most a quarter of the connections) in all: past them a long poll is answered at once, `429` (its client's) or `503` (everyone's), `J032`, with `Retry-After: 2` and the job's status, and an event stream is refused, `503`. Four connections past the connection cap are held for health checks alone, one request each, for 2 s. The service raises its open-file limit to its hard limit as it starts (on macOS no further than `kern.maxfilesperproc` and 10240), says so in its log, and holds at most `WAI_QSVC_CONNECTIONS` (4096) connections, fewer where that limit leaves less room: past them the connection idle longest (or the one whose body has been arriving longest) is closed for the new one, so a health check is always answered, and with none to close a new connection is answered `503` at once. - **Updating.** Readers and workers start from the service's own binary as it was started: on Linux its running image, elsewhere a copy it keeps beside its signing key, named by its digest. So the binary can be replaced before the restart (by a copy over it, or, on Linux, where a running binary cannot be written, by a rename), and the running service goes on with its own until it is restarted. ## Sending a job again A connection can fail after the service took a job and before its answer came back. To send the job again safely, give `POST /v1/jobs`, `POST /v1/run` or `POST /v1/write` an `Idempotency-Key` (the same working group's field): 1 to 255 visible ASCII characters, with no quote or backslash, as a structured field's string or bare. Make it unguessable, a UUID or 128 random bits, since it is kept per client and a client is an address. - Sent again with the same key and the same request, the job is answered with the job the key names, before any limit, and counts as no submission. A send that overtakes its first, both still being read, is read and counts, and gets the one job. - The same key with another request is refused, `422` and `J034`. - A key names its job for as long as the job is kept, whatever its status, with one exception: a job the service interrupted. That job's key runs the request again. - The record keeps a digest of the key with the client's name, never the key. The MCP tools that run (`run_quantum_circuit`, `run_quantum_set`, `run_on_target`, `write_circuit`) take the same key as `idempotency_key`. Without one, each call is a new job, which is why their `idempotentHint` is false. The command line (`wai-quantum submit`) and the Python client send each job under a key of their own. Where no answer comes back, they send it again under that key, twice at most, and only to a service whose `GET /v1` says it keeps keys (`idempotency`). ## Kept across restarts Every job is in the service's store before its submission is answered, and again at each change of its status. Programs, requests, results and receipts are kept under the BLAKE3 digests of their bytes; a job's `digests` names them. When the service starts, it reloads the store: - a finished job is served as it was, its receipt still verifying; - a job that was running is run again, once, from its stored request: it is queued again in its order, and its `interruptions` say when the service stopped; - a job the service stops again while it runs once more comes back `interrupted`, with the reason, so a job that stops the service whenever it runs does not run for ever; - a queued one is queued again, in its order. A restart reads no program: a job keeps the plan it was admitted with, and a set its items, so what is queued delays no restart. One too large for the service's limits as they now stand fails, saying why. Finished jobs are kept for `WAI_QSVC_RETAIN_SECS` (a day here), then dropped. A record names its client by a keyed hash of the address, never the address. [docs/self-host.md](docs/self-host.md#the-job-store) describes the store. The six checks answer at once, each read and answered in a reading child as a job's admission is. Each counts against the client's submissions, as a job does, once it has its turn and before it reads anything (a check that waits ten seconds and finds no turn is refused, `503`, having read and spent nothing). At most four run at a time, one of them a client's (and one more lent for a quick call), its others waiting behind it. Its bounds and its answer then read its programs within one budget, each read once: a check that runs the budget out is refused, `422` and `J031`, whatever it had made of it, so a repair of many fixes to a slow program ends at the budget. Some checks are refused (`422`) with the command that runs them on your own machine: - an equivalence check that would apply more than 4·10⁹ gate amplitudes; - an improvement whose candidates' checks would apply more than that; - a thorough search for shortcuts; - a compile of more than 20,000 gates, or with more than 256 rotations to approximate, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum compile`). A job that asks to be compiled first is held to the same bounds; - an error-correction call past its bounds here (`wai-quantum qec`): a decode of more than 200,000 shots, of more than 1,024 fired detectors, of a memory experiment past distance 7 or 21 rounds, or of more than 2 MiB of circuit or model text; a threshold of more than 16 points or 200,000 shots in all, or at circuit level past distance 7 or 21 rounds; a decode or a threshold whose shots, measured as they run, would take more than `WAI_QSVC_QEC_SECS` (25 s); the distance certificate of `bb:288`; - a gradient, an optimisation, a VQE or a QAOA run past its bounds here (`wai-quantum gradient`, `optimize`, `vqe`, `qaoa`): more than 16 qubits, more than 500 steps, or about 4·10⁹ gate amplitudes in all; phase estimation on more than 12 counting qubits, or amplitude estimation on more than 8 (`wai-quantum build`); - an estimate of more than 200,000 gates, or with more than 256 distinct rotation angles to synthesise, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum estimate`). Verifying an estimate re-runs it, within the same bounds. - a pulse check whose run takes more than 2·10¹⁰ of the simulator's multiply-adds, or a pulse call on more than 1 MiB of program (`wai-quantum pulse check`). A pulse job itself is held to the limits every job is. ## For agents `POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `convert_program`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_components`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode`, `qec_threshold`, `pulse_schedule`, `pulse_check`, `gradient_qasm`, `optimize_qasm`, `vqe_ground_state`, `qaoa_maxcut`, `build_algorithm`, `run_landscape`, `run_qudit` and `run_wasmq` (a qudit job and a wasmQ envelope, each run as a job; [above](#wasmq-envelopes-and-qudit-jobs)), `check_quantum_program`, `grade_quantum_program` and `attest_quantum_job` (the sign-off loop; [above](#the-sign-off-loop)) and `write_circuit` (a program written for a task, on a service with a model). A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`): - resources: the language the engine reads (`wai-quantum://docs/language`), its diagnostics, the methods with their accuracy classes (their reach in a job's memory here), and worked examples, each with what it must do; - prompts: writing and verifying a circuit for an intent, explaining a result and its receipt, and repairing a program from its diagnostics. Each names this server's own tools. Every answer about a program states a verdict. Reading the program again for it costs what reading it cost, so it is read, in a reading child, only for a call that was counted. A poll (`get_quantum_job`) states what its job holds: the result's verdict where the job asked for one (`verdict` or `intent`); otherwise, and for a job not yet ended, that it reads, with empty `diagnostics` and `critique`, and a view on a poll draws nothing. A tool that says nothing about a program takes no turn. A call refused before it was counted, told to try again, or that finds no turn for its verdict states a verdict that gives the reason and nothing of the program. A client whose `initialize` says its host draws interactive views (the extension `io.modelcontextprotocol/ui`, version 2026-01-26) gets the engine's answers as views, as from the command line's server: each tool that has one names its `ui://wai-quantum/*` resource, the six views are listed and read, and each answer carries what its view draws in its `_meta`. The service keeps no session, so the client's word is carried in the session id it is given at `initialize` (`wq-views-` and 32 random hex digits, kept nowhere), which a client sends back with every request (`Mcp-Session-Id`); a client that draws no views gets no session id and none of this. For a job still running when the run's wait ends, the result view follows it with `poll_quantum_job`, which the host keeps from the model (`visibility: ["app"]`); a model reads a job with `get_quantum_job`. A client that speaks MCP 2025-11-25 may call the three runs and `write_circuit` as tasks (`tools/call` with `task`): `initialize` declares `tasks` (`cancel`, and `requests.tools.call`), and each one's `execution.taskSupport` is `optional`. A write's task says, while it works, how many attempts the engine has checked, and its result states the verdict of the last program it wrote. - **The task is the job.** Its `taskId` is the job's id. `tools/call` answers at once with the task, `working`. A program that does not read makes no task: the answer is an error (`-32602`) whose `data` is the refusal, with its codes and verdict (of the program where the request was counted, as above). - **Statuses.** `tasks/get` gives the job as a task: `working` while it is queued or running, `completed` once done, `cancelled`, or `failed` for a job that failed or that the service interrupted, with why. Its `ttl` is the service's retention, which a job outlasts from its creation, since it is kept that long after it ends. - **The result.** `tasks/result` gives what `get_quantum_job` gives for the job that ended, naming the task in its `_meta` (`io.modelcontextprotocol/related-task`), an error (`isError`) where the job failed. - Asked while the task still works, by a client that takes `text/event-stream`, the answer is an event stream: kept open, and the answer its one event when the job ends. - Any other client is held for up to 50 s, then told to ask `tasks/get`. - **Cancelling.** `tasks/cancel` cancels the job. A task that has ended is not cancelled (`-32602`). - **Who may read a task.** There is no `tasks/list`, and the public service binds a task to no one: a task's id, 128 random bits, is what reads it, as a job's is. On a private deployment that takes API keys, a task is its key's alone. `server.json` beside this README describes the endpoint in the MCP Registry's format; the registry lists it as `science.transaction.wai/quantum-jobs`. The command line's agent docs (`llms.txt`, `AGENTS.md`, the Agent Skill) list these tools from `wai/wai-quantum-cli/docs/service-mcp.json`, which this crate's tests hold to the live list. After changing a tool, run `WAI_QUANTUM_BLESS=1 cargo test mcp::` here, then regenerate the command line's docs (its README says how). ## Receipts Every finished result carries a `receipt`: an Ed25519 signature, by the key `GET /v1` publishes, over the BLAKE3 digest of the request as submitted and the result as returned. `POST /v1/verify` checks one, and so can any Ed25519 implementation: - digest = BLAKE3(`wai:quantum-service-receipt` ‖ 0x02 ‖ len(R) ‖ R ‖ len(S) ‖ S), each len a big-endian u64; - R and S are the RFC 8785 canonical texts (`"canon": "rfc8785"`) of the request and of the result without its `receipt`: keys sorted, no whitespace, every number written as ECMAScript writes the double it reads. So a result verifies after any JSON reader has carried it, a browser's included. Whole numbers in a request must be within ±(2^53 − 1), so that every reader reads them alike. Receipts issued before the canonical form carry no `canon` and still verify. The provider protocol's [§8](../extensions/quantum-providers.md#8-the-receipt) states the seal in full: the canonical text, the first form, how a job is read, the strict signature, and the reasons a verifier gives; [`quantum-conformance/receipts/`](../quantum-conformance/receipts/README.md) holds receipts and a second verifier written from that text. The receipt names its key, and `GET /.well-known/wai-quantum-receipt-keys` lists every key the service has signed with, each with its validity window. `wai-quantum-service rotate-key` retires the key: the old one stays listed with its window closed, and a transition statement naming both keys is signed by each ([docs/self-host.md](docs/self-host.md#the-receipt-keys)). Each receipt also seals when it was issued (`time.issued_ms`, milliseconds since 1970 by the service's clock), inside its digest, so the time cannot be changed or taken off; with `WAI_QSVC_RECEIPT_VALID_S` it seals until when it is offered too (`time.not_after_ms`). The published key set is a trust list as a verifier reads one: `wai-quantum verify job.json --trust keys.json --at now` holds a receipt to its key's window by the time it seals, refuses a key the set marks revoked (`wai-quantum-service revoke-key KEY [FROM_MS]`), and says when a receipt has expired (the provider protocol's [§8.8](../extensions/quantum-providers.md#88-whose-key-and-when)). The time is the service's word, signed. A verifier from before receipts sealed a time reports `digest_mismatch` on one that does; `WAI_QSVC_RECEIPT_TIME=none` seals none, for a deployment whose readers still run one. Each new result also carries `provenance`, sealed with it: the program's digest and size, the method and its accuracy, each pass a compile ran with its check's verdict and the certificate's digest, the shots, seed and plan, and the engine's and the service's versions. [docs/provenance.md](docs/provenance.md) maps every field to the categories provenance surveys use. ## Energy Every finished result carries `energy`, inside the body the receipt seals: the CPU energy the job's worker process spent, from its start to its exit. The service reads it from the kernel's per-process count (`proc_pid_rusage`, `ri_energy_nj`, on macOS) after the worker exits and before it is reaped. An exited process has left every core, so its count is complete. ```json fragment "energy": { "joules_micro": 2540113, "energy_provenance": "OnChipCounter", "energy_uncertainty": { "relative_ppm": 1, "window_us": 2603412 }, "energy_counter_filtering": "Unknown", "coverage": "whole", "attribution": "process", "covers": "the CPU energy of the job's worker process, from its start to its exit, with the CPU energy system services billed to it", "excludes": ["memory", "the host's idle draw"], "cpu_ms": 31680, "source": "proc_pid_rusage ri_energy_nj + ri_billed_energy − ri_serviced_energy, read after the worker exited and before it was reaped" } ``` The fields follow WAI's energy-measurement extension (§2): - `energy_provenance` is the acquisition class. `OnChipCounter` means the silicon's own accounting, with no calibration behind it. - `energy_uncertainty.relative_ppm` declares the part of the uncertainty that can be evaluated: here, the rounding to whole microjoules. It is not the figure's total error, because the counter's own systematic error is not evaluated. - `window_us` is the worker's lifetime. - `energy_counter_filtering` is `Unknown`, because the platform does not document whether the count is filtered. A `statevector_single` or `cutrank_single` job runs on the GPU, which this counter does not read, so its figure is `partial` and says so in `excludes`. So is a figure that includes CPU energy system services billed to the worker, since such credits land late. On Linux there is no per-process count. The service reads the processor packages' energy counters instead, through the kernel's power-capping interface, before the worker starts and after it exits. They count whole packages, so the figure is the packages' energy over the job's window, every process on the host included. It says so: `attribution` is `none` (the macOS figure's is `process`), and `concurrent_jobs` counts the other jobs that ran in the window. The counter is used only when every package zone reads and is live (each package advancing in 80% of fifteen 20 ms intervals at start). Its declared uncertainty covers the two whole-microjoule reads and the counter's update edges, measured then, which also bound the hardware unit the interface hides. A write's figure is the writer process's. The writer runs on between writes, so the service reads its count before the write is sent and again after its last line comes back, over the write's window. The second read trails by the time the writer's threads are still on a core, which the kernel adds only when they leave it, so the figure is a lower bound by that much: its `coverage` is `partial`, and `excludes` says why. On Linux a write's figure is the packages' over its window, as a job's is. Where the service cannot read a count (another system, a zone its user may not read, a counter that is not live, as in most containers), `energy` is `{"unmetered": ""}`. It is never a figure of zero. `GET /v1` says how this host meters (`energy`). A set's figure covers every item together; each item's own says so. `wai-quantum-service run FILE` runs a job in its own process and reports no energy. ## Versions `GET /v1` and `quantum_service_info` list the engine's components (`components`, `wai.quantum.components/1`, which `list_components` also gives): every method, compiler pass, noise channel and format the service runs through, each with its version and the digest of its descriptor. The service runs the built-ins alone and never loads a component from a request ([the extension points](../wai-quantum/docs/extension-points.md)). `GET /v1` names the service's version and gives `versions` (`wai.quantum.versions/1`): the engine's version, the service's, the schema of each document it writes and the MCP protocol versions it speaks. The MCP server gives the same in `initialize` (`_meta["science.transaction.wai/versions"]`) and in `quantum_service_info`. Every result names the engine that made it and its schema (`engine`, `schema`), inside what its receipt seals. Each MCP tool's `outputSchema` is its answer's schema; this server's own are in [`schemas/service-tools.v1.json`](schemas/service-tools.v1.json), the job's and the engine's in [`wai-quantum/schemas/`](../wai-quantum/schemas). What is stable and for how long: [the policy](https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-quantum/VERSIONING.md); what is deprecated: [the deprecations](https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-quantum/DEPRECATIONS.md). ## Running it ```sh norun=ci:wai-quantum-service.yml cargo build --locked --release WAI_QSVC_ADDR=127.0.0.1:18241 ./target/release/wai-quantum-service serve & # serves until stopped bash smoke.sh http://127.0.0.1:18241 ``` Each job runs in a child process of the same binary (`worker`), at a lower scheduling priority than the host's other work, and is watched for its resident memory and its time. A worker whose service has gone stops itself. Jobs are kept in `WAI_QSVC_STORE` (by default `~/.local/share/wai-quantum-service/store`; `memory` keeps them only while the process runs). [docs/self-host.md](docs/self-host.md) covers running it yourself: the OCI image ([`deploy/Containerfile`](deploy/Containerfile)), the configuration, the store, the keys, API keys for a private deployment, and a reverse proxy in front. [docs/scheduler.md](docs/scheduler.md) covers handing the service to an on-premises batch scheduler's jobs. On macOS the worker runs in a sandbox (`sandbox-exec`): no network, no file writes, and no program but itself. It reads its job on stdin and writes its result on stdout, so it needs none of them, and a flaw in parsing a submitted circuit then reaches neither the host's files, nor the network, nor a shell. The writer runs in the same sandbox, its model's directory read. `WAI_QSVC_SANDBOX=0` turns the sandbox off, for debugging. Limits are counted per client. A client is an IPv4 address, or the /64 an IPv6 address is in: one subscriber is usually given a /64 whole, so counting single addresses would hand out fresh limits to anyone who rotates through theirs. Behind the front proxy, the client is the first `X-Forwarded-For` address, which the proxy sets from the edge network's own client-address header rather than passing on what the request carried. These variables set the limits: | variable | default | meaning | |---|---|---| | `WAI_QSVC_BUDGET_GB` | 96 | memory all running jobs share | | `WAI_QSVC_JOB_GB` | 72 | memory one job may take | | `WAI_QSVC_WORKERS` | 4 | jobs running at once | | `WAI_QSVC_MIN_FREE_PCT` | 15 | share of the host's memory a starting job must leave free | | `WAI_QSVC_JOB_SECS` | 900 | the longest a job may run | | `WAI_QSVC_CLIENT_ACTIVE` | 3 | queued or running jobs one client may hold | | `WAI_QSVC_CLIENT_GB` | the job size | memory one client's queued and running jobs may hold together | | `WAI_QSVC_CLIENT_BURST` | 30 | submissions one client may make per window | | `WAI_QSVC_CLIENT_WINDOW_SECS` | 600 | the window | | `WAI_QSVC_READ_GB` | 4 | the memory one request's reading may hold, in its child | | `WAI_QSVC_READ_SECS` | 45 | the time one request's reading may take | | `WAI_QSVC_READ_ANSWER_MIB` | 128 | the most one request's reading may give back to the service, once read | | `WAI_QSVC_QEC_SECS` | 25 | the time an error-correction call's shots may take, measured as they run | | `WAI_QSVC_ANSWER_SECS` | 55 | the longest any answer takes, its waits and readings included | | `WAI_QSVC_CLIENT_KEPT_MIB` | 128 | what one client's ended jobs may keep in memory | | `WAI_QSVC_KEPT_MIB` | 4096 | what all ended jobs whose results have been read may keep in memory | | `WAI_QSVC_CLIENT_UNREAD_MIB` | 256 | what one client's results not yet read may hold, each of its running jobs counted at its result's bound and the next at `WAI_QSVC_RESULT_MIB`; past it, its next job is refused (`429`) | | `WAI_QSVC_UNREAD_MIB` | 8192 | what all results not yet read may hold; past it, every next job is refused (`503`) | | `WAI_QSVC_RESULT_MIB` | 64 | the most one job's or set's result may take, as served: a request whose result's bound passes it is refused before it runs (`422`, `J031`) | | `WAI_QSVC_CLIENT_STREAMS` | 4 | long polls (`?wait=`) and event streams one client may hold at once | | `WAI_QSVC_STREAMS` | 512 | those of all clients, at most a quarter of the connections held | | `WAI_QSVC_KEPT_MIN_SECS` | 600 | how long an ended job not yet read is kept whatever the two caps above say | | `WAI_QSVC_CLIENT_VERIFIES` | 120 | receipts one client may have checked in its window (`POST /v1/verify`, `verify_quantum_receipt`) | | `WAI_QSVC_CLIENT_REQUESTS` | 32 | requests one client may have open at once; past it, `503` before the body is read | | `WAI_QSVC_CONNECTIONS` | 4096 | connections held at once, at most; lowered to what the open-file limit leaves room for | | `WAI_QSVC_CLIENT_READS` | 2 | how many of one client's jobs, dry runs and verdicts are read at once (more wait) | | `WAI_QSVC_CLIENT_CHECKS` | 1 | how many of one client's checks run at once (more wait) | | `WAI_QSVC_HEADER_SECS` | 150 | how long a client may take to send a request's header section, and how long an idle connection stays open | | `WAI_QSVC_BODY_SECS` | 30 | how long a client may take to send a request's body | | `WAI_QSVC_MAX_QUEUE` | 256 | circuit jobs waiting at once, and writes waiting at once, each counted alone | | `WAI_QSVC_RETAIN_SECS` | 86400 | how long finished jobs are kept, across restarts | | `WAI_QSVC_STORE` | `~/.local/share/wai-quantum-service/store` | the job store's directory, or `memory` | | `WAI_QSVC_API_KEYS` | unset | a file of API keys' digests: set, every request but the open ones needs a key (private deployments; the public service takes none) | | `WAI_QSVC_ENERGY` | 1 | 0 meters no job's energy | | `WAI_QSVC_TRUSTED_PROXIES` | `127.0.0.1,::1` | peers whose `X-Forwarded-For` names the client | | `WAI_QSVC_SANDBOX` | 1 | run each worker, and the writer, sandboxed (macOS); 0 runs them unsandboxed | | `WAI_QSVC_MODEL` | unset | the model's directory (its files and `wai-model.json`): set, the service writes programs for tasks (`POST /v1/write`); unset, it refuses them (`J080`) | | `WAI_QSVC_WRITER_GB` | 96 | the memory the writer process may hold, its model's mapped pages among it | | `WAI_QSVC_WRITE_SECS` | 3600 | the longest a write may run | | `WAI_QSVC_WRITER_SCRIPT` | unset | a script the writer runs in the model's place (tests) | | `WAI_QSVC_KEY` | `~/.config/wai-quantum-service/signing.key` | the signing key, created on first start; its key set beside it, `.keys.json` | | `WAI_QSVC_RECEIPT_TIME` | `clock` | `clock`: every receipt seals when it was issued, by this host's clock; `none`: receipts seal no time | | `WAI_QSVC_RECEIPT_VALID_S` | unset | a lifetime in seconds: every receipt also seals the moment that long after its issue, from which a verifier held to a time says it has expired | `wai-quantum-service run job.json` runs one job or set in the current process and prints its plan and its result. `wai-quantum-service rotate-key` retires the receipt key and makes a new one. `wai-quantum-service api-key NAME` makes an API key and the line for the keys file.