State machines
A state machine is the computational backend for a single user.
The Zeq framework is, first, a state machine substrate — your hash-linked, Zeqond-stamped backend where computation actually runs. Sitting on top of that substrate is a new mathematical language — operators, the master equation, KO42 — that you compose against. Your compute calls come out of your state machine. Your audit entangled state is your state machine. Your encryption boundary, your two API keys (the zeq_ak_* you build applications with and the zsm_* your devices post observations through), and the per-call regulatory envelopes are all bound to it. One user, one state machine.
This page is the concept. The wire-level walk-through is in Spin up your first state machine.
Anatomy
A state machine has six things attached to it:
- An owner ZID (
ZEQ07XXXXXXXXX) — the equation-derived identity that created it. - A slug (
[a-z0-9-]{1,64}) — the public, URL-routable handle. Every chain endpoint scopes via/:slug. - A genesis Zeqond — the integer Zeqond number at spin-up. Used as the seed for the entangled state's first
prev_hash. - A parent origin — the tenant origin (
zeq-dev, your self-hosted origin, etc.). Lets the framework run multi-tenant without slug clashes. - A visibility flag —
is_publicexposes the read endpoints (/state,/explore,/explore/sse,/pohc/validate, the aggregator stream) without auth. Private machines require viewer role or above. - At least one API key — minted on spin-up, prefixed
zsm_, hashed at rest. Every call attributable.
The DB row for this lives in state_machines (alongside state_machine_api_keys, state_machine_roles, audit_log, contracts, contract_transitions, tally_supply, tally_tokens).
Roles
Five roles, in increasing authority:
| Role | Read | Write events | Submit transitions | Manage keys | Manage roles | Delete machine |
|---|---|---|---|---|---|---|
none | public only | — | — | — | — | — |
viewer | yes | — | — | — | — | — |
operator | yes | yes | yes | — | — | — |
admin | yes | yes | yes | yes | yes | — |
owner | yes | yes | yes | yes | yes | yes |
Roles are stored per-state-machine — the same ZID can be owner of one machine, operator of another, viewer of a third. The middlewares requireAuth (rejects anonymous) and optionalAuth (allows public reads when is_public=true) gate every chain endpoint; helpers canRead(machine, zid) and canWrite(machine, zid, "operator") enforce the matrix.
API key scopes mirror this: viewer, operator, admin. A minted key carries one scope; a key that's viewer can never write. Owners always hold at least one admin-scope key.
The entangled state
Every state machine has its own audit log — the rows in audit_log filtered by originId = parentOrigin:slug. Each row has:
{
id: bigserial
originId: "zeq-dev:my-iot-fleet"
zeqondNumber: bigint // when it happened, framework time
unixTimestamp: numeric // mirror in Unix seconds
phase: real in [0,1) // (unix mod 0.777) / 0.777
stateHash: char(64) hex // sha256(payload) or supplied hash
prevHash: char(64) hex // chains back to the previous row
transitionId: uuid // unique per row
transitionType: text // "event"|"state"|"file"|"connection"|"contract"|...
proofDigest: char(64) hex? // when the row carries a ZeqProof
zspEnvelope: jsonb? // when the row carries a ZSP-sealed payload
createdAt: timestamptz
}
The first row's prevHash is the genesis seed — sha256(slug || ":" || genesisZeqond). Every row after that links: prevHash[N] = stateHash[N-1]. Tampering with any row breaks the entangled state at that row's successor.
Validation is one call:
curl -sS https://zeqapi.com/api/chain/my-iot-fleet/pohc/validate \
-H "Authorization: Bearer ${ZSM_KEY}"
The handler in chain.ts walks the rows in order, recomputes each prevHash, and reports:
{ "ok": true, "valid": true, "count": 17, "broken_at": null,
"genesis_zeqond": "2287439210" }
Validation runs in time linear in the row count and is bounded by ?from=A&to=B if you want to spot-check a window.
What gets stored vs. what gets hashed
The framework runs on a hash-commitment-only doctrine for free-tier machines.
- The
event,state,file, andconnectionwrite endpoints accept either a pre-computedhash(preferred — bytes never cross the wire) or apayload(server hashes it and discards the bytes). - The entangled state row only persists the 64-char hex digest, the type metadata, the Zeqond number, and the previous hash.
- Raw bytes are NEVER stored unless the caller explicitly sends a HITE-sealed
sealed_eventenvelope — and even then, the envelope is sealed with the caller's key, not the framework's. - Logs emit a
clientIdHashand the Zeqond — never the raw ZID, IP, or payload.
This is the AT-REST-ENCRYPTION-POLICY. The framework proves that something happened at Zeqond N with payload-hash h. You keep the bytes. The entangled state is the witness.
If you want server-side bytes — for a hosted contract that needs to read the input later, for example — you opt in via the contract engine, which wraps the payload in a ZSP envelope before persisting (see Contracts).
How a state machine binds to an API key
The binding is 1:1, enforced at three layers:
- Mint.
POST /api/chain/:slug/keys(or auto-mint at spin-up) writes a row instate_machine_api_keyswithstate_machine_id = machine.id. The raw key is shown once; onlysha256(raw)is persisted. - Resolve. Every authenticated route runs
authenticateKeymiddleware: it hashes the bearer, looks up the row, joins to the state machine, populatesres.locals.authZid,res.locals.authPlan,res.locals.authStateMachineId. - Enforce. Every write helper (
gateWriteinchain.ts, the contract router, the tally router) checkscanWrite(machine, zid, requiredRole)against the resolved ZID. A key minted for one machine cannot write to another, even if you put it in the wrong header.
This is why the doctrine is "every compute comes out of a state machine" — there's no privileged admin key floating outside the binding. Even self-hosted setups follow this; the dev auth-v3 flow auto-creates a state machine and an admin key on registration so you're never key-less.
Public vs. private machines
A is_public: true machine exposes its read surface (state, block, explore, explore/sse, pohc/validate) to anonymous callers and gets fanned out into the public aggregator stream at /api/chain/aggregate/sse. zeqstate.com's observer subscribes to that single stream rather than opening N per-machine SSE connections.
A is_public: false machine returns 401/403 on read attempts without a viewer-role token. Private machines are still on the entangled state — their hashes commit into the same audit log — they just don't appear in the public registry or the aggregate stream.
Toggle is an entangled state-admin action: PATCH /api/chain/:slug with {"is_public": true}.
What it means in practice
When you build on the framework, you stop thinking in users and start thinking in state machines.
- Building an IoT fleet? One state machine per device, or one per fleet. Each device's heartbeat is a transition; the entangled state is the audit trail.
- Building an LLM agent? One state machine per agent. Every prompt → every model call → every tool call is a transition. The entangled state proves what the agent saw and what it did.
- Building a marketplace? One state machine for the contract template, instances per deal. Each deal's escrow → release → settle is a contract transition with a tally token minted on each successful step.
- Embedding analytics on a marketing site? One state machine for the site. Beacons hit
/api/embed/:slug/ingest; transitions land on the entangled state; the dashboard reads the entangled state, not a third-party warehouse.
One key per machine. One chain per machine. One owner per machine. The framework's authority surface is exactly that: this row, signed by this owner, at this Zeqond. Everything else is a function of that.
Next
- Spin one up — Quickstart
- The 7-step Wizard protocol — learn/seven-step-protocol
- Programmable state machines on top — Contracts
- Receipts on transition — Tally tokens
- Universal embed — Embed snippet
- API surface — chain endpoints