ZeqReactor
Where ZeqStream broadcasts one sealed envelope, a ZeqReactor closes the loop. The sealed output of step n, combined with a live environment observation, becomes the query that computes step n+1:
xₙ₊₁ = seal( Solver( T(xₙ, uₙ) ) ), prev_hash(xₙ₊₁) = zeqProof(xₙ)
xₙ— the sealed CKO envelope at step n (value, field, R(t), master-equation terms, functional E, spectral Ψ, ZEQOND receipt, proof).uₙ— the environment observation your simulation pushes in.T— the transition contract: a declarative map from (output, environment) to the next solver's inputs.
Every one of the 26 solvers can drive a reactor. Each step is sealed, hash-linked
into the entangled-state chain, carries physics telemetry, and is reality-gated
— a step that fails its ≤0.1% check can halt the loop (the seal is the stability
monitor). See the full design in ZEQREACTOR-ARCHITECTURE.md.
The API
| Method | Endpoint | What it does |
|---|---|---|
| POST | /api/zeq/reactor | create a reactor from a transition contract |
| GET | /api/zeq/reactors | list your reactors (owner-scoped) |
| POST | /api/zeq/reactor/:id/step | advance one step (optional inline env) |
| POST | /api/zeq/reactor/:id/observe | push an observation; if trigger:on-observe, step now |
| GET | /api/zeq/reactor/:id/state | the latest sealed state |
| GET | /api/zeq/reactor/:id/trajectory | the hash-linked chain of states (+ chainLinked) |
| GET | /api/zeq/reactor/:id/telemetry | per-step R(t), functional E, spectral Ψ, chaos, coherence, verdict |
| GET | /api/zeq/reactor/:id/next?after=N | long-poll: resolves with the next state after step N (live subscribe) |
| POST | /api/zeq/reactor/:id/secure/open | open a ZeqSSH encrypted line (one-time sessionSecret) |
| GET | /api/zeq/reactor/:id/secure/pull?session= | keyless AES-256-GCM frame of the latest sealed state |
| GET | /api/zeq/reactor/:id | status (contract, step, budget) |
| POST | /api/zeq/reactor/:id/close | stop the reactor |
step and observe are computes — they need a zeq_ak_ key and burn credit,
tied to your ZID. Reads are owner-scoped. To hand sealed states to another
piece of software with no API key, open a ZeqSSH line (below) — the consumer
pulls a keyless URL and decrypts with a secret it holds in memory.
There's a full app for this at zeq.dev/reactor — build a contract, drive it from any external data source, watch the sealed trajectory live, and open a ZeqSSH line, all without touching a key.
The transition contract
{
"solver": "kalman", // any solver key, or "auto"
"initialInput": { "steps": 60, "sigmaZ": 0.5, "v0": 1.0 }, // x₀ base inputs
"transition": {
"inputs": {
"sigmaZ": "env.noise", // inject environment
"v0": "output.rmse" // carry the previous output forward
}
},
"trigger": { "mode": "on-observe" }, // on-request | on-observe
"guards": {
"maxSteps": 5000,
"creditBudget": 1287,
"until": "output.value < 1e-6", // terminal predicate
"onUnverified": "halt" // halt | continue
}
}
Each transition.inputs value is either a literal or a dot-path over the
read-only namespace { output, env, step } (a string starting with output,
env, or step). On step 0 there is no output, so output.* paths are dropped
and the base initialInput seeds the first state.
Field carry (stateful twins). A solver that accepts an initialState field
can be handed its previous field as the next initial condition — e.g. a tumour
front that keeps advancing, or a temperature field that keeps evolving:
{ "solver": "tumor",
"initialInput": { "D": 0.5, "r": 1.0, "nx": 400, "L": 60, "steps": 400 },
"transition": { "inputs": { "initialState": "output.finalDensity", "r": "env.growthRate" } },
"trigger": { "mode": "on-observe" } }
Verified live: the front advances 13.7 → 21.0 → 25.0 → 34.3 across steps as the
density field carries forward, each step sealed and bounded (u ∈ [0,1]).
Stability — the seal is the monitor
Each step seals verified only when it passes its own ≤0.1% reality check. If a
step drifts (observable-differential), the onUnverified guard decides:
halt (safe default for control), or continue (exploration). A blown-up step
never seals and always halts — a garbage state can never seed the next one. The
per-step telemetry (R(t), functional E, spectral Ψ, chaos, coherence) lets
you watch the loop's health in physics terms: a Lorenz reactor pushed past the
chaos onset shows its Lyapunov value cross zero and its coherence collapse.
Provenance
Every step is a normal sealed compute in the entangled-state chain, and the
reactor records each step's proof linked to the previous (prev_state_proof).
GET /:id/trajectory returns the chain and a chainLinked flag. Given the initial
input, the contract, and the environment log, the whole trajectory is replayable
and independently verifiable — a cryptographically verifiable record of a live,
environment-driven simulation.
Consume sealed states over ZeqSSH — no API key
Driving a reactor is a compute (it needs your zeq_ak_ key). But handing the
sealed states to another system — a dashboard, a device, a partner's simulator —
should not require sharing a key. Open a ZeqSSH line instead:
# owner opens the line once (authenticated) → a one-time sessionSecret
curl -sX POST https://www.zeq.dev/api/zeq/reactor/$RX/secure/open -H "Authorization: Bearer $ZEQ_KEY"
# → { sessionId, sessionSecret, cipherSuite, pull }
The consumer then pulls the keyless URL and decrypts each frame with the
secret it holds in memory — no API key, no .env. Keys rekey per Zeqond
(forward secrecy); a leaked sessionId alone yields only ciphertext.
import crypto from "crypto";
const f = await (await fetch(PULL_URL)).json(); // { zeqond, iv, ct, tag }
const info = Buffer.from("zeq-ssl-record/s2c/" + f.zeqond);
const key = crypto.hkdfSync("sha256", Buffer.from(SECRET,"hex"), Buffer.from(SID,"hex"), info, 32);
const dec = crypto.createDecipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(f.iv,"hex"));
dec.setAuthTag(Buffer.from(f.tag,"hex"));
const state = JSON.parse(dec.update(Buffer.from(f.ct,"base64")) + dec.final()); // the sealed reactor state
Drive it from your software
Driving a reactor needs your zeq_ak_ key. Inject it at runtime from ZSC (the
Zeq Secure Store), your secret manager, or — the target model — the machine's
MMIO region. The $ZEQ_KEY / process.env.ZEQ_KEY below is a runtime-injected
secret, never a committed .env (the framework's rule; the consumer side is
keyless anyway). See the MMIO connection architecture for where this is going.
curl — one loop iteration
# create
RX=$(curl -sX POST https://www.zeq.dev/api/zeq/reactor -H "Authorization: Bearer $ZEQ_KEY" \
-H 'Content-Type: application/json' -d '{"contract":{"solver":"kalman",
"initialInput":{"steps":60,"sigmaZ":0.5},"transition":{"inputs":{"sigmaZ":"env.noise"}}}}' \
| jq -r .reactorId)
# step with live environment data
curl -sX POST https://www.zeq.dev/api/zeq/reactor/$RX/step -H "Authorization: Bearer $ZEQ_KEY" \
-H 'Content-Type: application/json' -d '{"env":{"noise":0.8}}' | jq '{step,value,verified}'
JavaScript — the feedback loop
const H = { "Content-Type": "application/json", Authorization: `Bearer ${ZEQ_KEY}` };
const { reactorId } = await (await fetch(`${API}/api/zeq/reactor`, { method:"POST", headers:H,
body: JSON.stringify({ contract }) })).json();
setInterval(async () => {
const env = readSensors(); // your environment
const s = await (await fetch(`${API}/api/zeq/reactor/${reactorId}/step`,
{ method:"POST", headers:H, body: JSON.stringify({ env }) })).json();
applyToSimulation(s); // sealed next-state
}, 777); // one Zeqond
Python — MPC / digital twin
rx = requests.post(f"{API}/api/zeq/reactor", headers=H, json={"contract": contract}).json()["reactorId"]
while running:
env = measure() # environment
x = requests.post(f"{API}/api/zeq/reactor/{rx}/step", headers=H, json={"env": env}).json()
actuate(x["value"]) # sealed next-state
Live subscribe (a dashboard/device that only reads) — long-poll next:
let after = -1;
for (;;) {
const r = await (await fetch(`${API}/api/zeq/reactor/${id}/next?after=${after}`, { headers:H })).json();
if (r.done) break;
if (r.state) { render(r.state); after = r.state.step_index; }
}
TypeScript SDK — ZeqReactorClient
The @zeq/sdk package ships a typed client so you don't hand-roll the HTTP:
import { ZeqReactorClient } from "@zeq/sdk";
const reactor = new ZeqReactorClient({ apiKey: process.env.ZEQ_KEY });
// create from a transition contract
const { reactorId } = await reactor.create({
solver: "kalman",
initialInput: { steps: 60, sigmaZ: 0.5, v0: 1.0 },
transition: { inputs: { sigmaZ: "env.noise" } }, // inject live data
guards: { onUnverified: "halt" },
});
// drive the loop from your environment
setInterval(async () => {
const s = await reactor.step(reactorId, readSensors()); // ← your data
applyToSimulation(s); // sealed next-state
}, 777);
// or subscribe as an async iterator (read-only dashboard/device)
for await (const state of reactor.watch(reactorId)) render(state);
// hand sealed states to another system with NO API key (ZeqSSH)
const line = await reactor.secureOpen(reactorId);
const sealed = await ZeqReactorClient.securePull(line.pull, line.sessionSecret, line.sessionId);
create / step / observe / close / secureOpen need your zeq_ak_ key;
securePull is keyless — the sessionSecret is the only credential. Full methods:
create · list · step · observe · state · trajectory · telemetry · next · watch · secureOpen · close.
Connect with no key in your code (MMIO). Run the local zeqd agent (see
zeqd/README.md) and use connect() — it prefers the machine's memory-mapped
region (the zeq_ak_ key is resolved from a revocable handle per request, never
a .env), and only falls back to an env key with a deprecation warning:
import { ZeqReactorClient, ZeqMachine } from "@zeq/sdk";
const m = await ZeqMachine.map(); // live: m.zid, m.origin, m.zeqond, m.credit
const reactor = await ZeqReactorClient.connect(); // MMIO-first; no key, no .env
await reactor.create({ solver: "kalman", initialInput: { steps: 60 },
transition: { inputs: { sigmaZ: "env.noise" } } });
connect() is the recommended path; fromMMIO() forces MMIO; new ZeqReactorClient({ apiKey }) still works for an explicit key.
Read next
- ZeqStream — broadcast a single sealed state contract.
- The equations on every envelope — what each step carries.
- All 26 solvers — any of them can drive a reactor.