Skip to main content

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 API keys (zsm_*, carrying a scope, and the zeq_ak_* publish key minted alongside it) and the per-call regulatory envelopes are all bound to it.

This page is the concept. The wire-level walk-through is in Spin up your first state machine.


One machine per owner — per node

You will read "one user, one state machine" in older documents. That rule is real, but its scope is narrower than the sentence suggests. createStateMachine() opens with:

// F9: one machine per owner. If the ZID already has one, return it.
const ownerExisting = await resolveByOwner(input.ownerZid);
if (ownerExisting) return { ok: false, machine: ownerExisting, error: "owner_has_machine" };

resolveByOwner queries this node's own Postgres. Each of the eighteen nodes has its own database, so the same ZID arriving at a second node passes the check and gets a second machine. Slug uniqueness (resolveBySlug) is local for the same reason.

So: one machine per owner per node, and a ZID can own machines on several nodes. That is not a bug you should route around — GET /api/identity/home/:zid returns homes as a list and sums totals across every node that answers, precisely because this state is reachable. See your identity and your home node.

Two consequences worth internalising:

  • Signing in somewhere new and finding nothing does not mean your machine is gone. It means you are on a node that does not home you. Creating another machine there gives you a second machine and splits your state across two homes. Ask the directory first.
  • Genesis still converges even when the rows don't. Before minting, a node calls readCanonicalGenesis(slug) over worm_ledger ∪ replica_ledger and adopts the earliest recorded machine_genesis for that slug. A slug born anywhere on the mesh keeps one birth Zeqond network-wide — deterministic, no consensus. What does not converge is the machine's contents, which never replicate at all.

Anatomy

A state machine has six things attached to it:

  • An owner ZID (ZEQ7XXXXXXXXXXXXXXXXXXXXXXXXXX) — the equation-derived identity that created it.
  • A slug ([a-z0-9-]{1,64}) — the public, URL-routable handle. Every state 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 originthe origin of the node that created it, read from ZEQ_ORIGIN: const parentOrigin = String(body.parent_origin || process.env.ZEQ_ORIGIN || "zeq-dev"). zeq-dev is only the fallback when nothing is configured, not the value on the live fleet — machines born on zeq.me carry zeqme, on zeqproof.com zeqproof, on zeqstate.com zeqstate. You can see it in the public aggregator: curl -s https://zeqproof.com/api/chain/aggregate/list reports "node": "zeqproof". It is what lets the framework run multi-tenant without slug clashes.
  • A home node — the homeNode column, set to the creating node's ZEQ_NODE_ID. A peer that later projects this identity from the replicated machine_genesis event stores the same value, so exactly one node ever ticks and mints for a given machine.
  • A visibility flagis_public exposes 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:

RoleReadWrite eventsSubmit transitionsManage keysManage rolesDelete machine
nonepublic only
vieweryes
operatoryesyesyes
adminyesyesyesyesyes
owneryesyesyesyesyesyes

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 state endpoint; helpers canRead(machine, zid) and canWrite(machine, zid, "operator") enforce the matrix.

API key scopes are a separate, smaller set: STATE_MACHINE_API_KEY_SCOPES = ["read", "write", "admin"], defaulting to read when a mint doesn't name one. A minted key carries exactly one scope; a read key can never write. Don't confuse the two vocabularies — roles are per-ZID on a machine, scopes are per-key.

Roles are also per-node, like everything else here: being owner on zeq.me grants you nothing at zeqproof.com, because that node has its own state_machine_roles table.


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: "zeqme:my-iot-fleet" // <parentOrigin>:<slug> — the origin is the node's
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 seedsha256(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 __ZEQ_BASE_URL__/api/state/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, and connection write endpoints accept either a pre-computed hash (preferred — bytes never cross the wire) or a payload (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_event envelope — and even then, the envelope is sealed with the caller's key, not the framework's.
  • Logs emit a clientIdHash and 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:

  1. Mint. POST /api/state/:slug/keys (or auto-mint at spin-up) writes a row in state_machine_api_keys with state_machine_id = machine.id. The raw key is shown once; only sha256(raw) is persisted.
  2. Resolve. Every authenticated route runs authenticateKey middleware: it hashes the bearer, looks up the row, joins to the state machine, populates res.locals.authZid, res.locals.authPlan, res.locals.authStateMachineId.
  3. Enforce. Every write helper (gateWrite in chain.ts, the contract router, the tally router) checks canWrite(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 that node's public aggregator stream — /api/chain/aggregate/sse, also served at /api/state/aggregate/sse. An observer subscribes to that single stream rather than opening N per-machine SSE connections.

The stream is node-scoped, not fleet-wide. GET /api/chain/aggregate/list on 2026-09-03 returned 3 machines all tagged "node": "zeqme" on zeq.me, 2 tagged "zeqproof" on zeqproof.com, and 3 tagged "zeqstate" on zeqstate.com. To watch the whole mesh you subscribe to all eighteen.

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/state/: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/state/:slug/event; transitions land on the entangled state; the dashboard reads the entangled state, not a third-party warehouse.

One entangled state per machine. One owner per machine, on that node. 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