Skip to main content

State Contracts

A state contract is deployable JSON logic that lives inside your state machine and runs on your machine's clock. You declare states, the transitions between them, and the operator that executes each transition. Once deployed, the contract advances in one of two ways: you drive it with a POST .../transition call, or it drives itself — a transition can carry a trigger (a recurring Zeqond interval, a cron schedule, an inbound event, an aggregate over a window) and the scheduler, ticked once per Zeqond, fires it with no external driver.

Every transition — driven or autonomous — is one atomic unit of work: condition check, operator compute through the master equation (compute → prove → verify), ZSP envelope, durable contract_transitions row, current_state update, and a row appended to your machine's entangled state. That entangled-state row carries the full kernel envelope, so the Observer can render the equation, the operator chain, and the verifier digest without re-running anything: the row's contract_context identifies {contractId, fromState, toState}, and master_equation_block is the verbatim equation the operator executed. A tally token is minted per successful fire.

Author contracts in the Contract IDE (Generate / Templates / Expert modes, plus the Saved home for everything you've deployed), or POST JSON directly. Watch them run in the Observer/state/?slug=<your-machine> has a Contracts section listing every contract and its fires on your entangled state.


The shape of a contract

States are named regimes (exactly one initial, any number terminal). Transitions move between them, each carrying an operator and an optional condition. This is a real catalogue template — force-threshold-alarm, the structural-load watchdog (NM19, F = ma):

{
"name": "force_threshold_alarm",
"version": "1.0",
"states": {
"idle": { "initial": true },
"watching": {},
"alert": {},
"cleared": { "terminal": true }
},
"transitions": [
{ "from": "idle", "to": "watching", "operator": "KO42", "proof_required": false },
{ "from": "watching", "to": "alert", "operator": "NM19",
"condition": "input.mass_kg * input.acceleration_ms2 > input.force_limit_n",
"proof_required": true },
{ "from": "alert", "to": "watching", "operator": "NM19",
"condition": "input.mass_kg * input.acceleration_ms2 <= input.force_limit_n",
"proof_required": true },
{ "from": "alert", "to": "cleared", "operator": "KO42",
"condition": "input.acknowledged == true", "proof_required": false }
]
}

Validation is two-stage and happens at deploy time: ContractDefinitionSchema.parse (shape) plus validateContractDefinition (semantics — every operator must exist in the live registry). The server stamps created_by with the caller's ZID; you don't send it. KO42 is the always-on substrate operator; physics operators like NM19 carry proof_required: true so each fire runs the full compute → prove → verify path.

Tamper evidence: at deploy, the framework stores a canonical SHA-256 of the definition (definition_sha256, deep-sorted keys). Before every transition the stored definition is re-hashed; a mismatch returns 409 definition_tampered and blocks execution. GET .../integrity exposes the check publicly.

Conditions — the safe-eval grammar

Conditions are a restricted expression language — comparison, arithmetic, &&/||, parentheses, and dotted identifiers (input.mass_kg, result.value). No function calls, no eval, no property tricks: the evaluator parses to an AST and rejects anything outside the grammar. A condition that evaluates false fails the transition cleanly (condition_failed), and the attempt is still recorded.

Triggers — make it autonomous

kindFires whenKey fields
recurringevery N Zeqonds, or on a cron scheduleevery_zeqonds or cron (+ cron_tz), max_fires?
one_shotonce, at a fixed Zeqondat_zeqond or at_unix
on_eventan emitted event landsevent, from_slug?, where?, into?
on_eventsseveral events combine (any / all)match, events[], within?
on_logica recursive AND/OR tree of events matchestree, within?
on_aggregatean aggregate over a window crosses a boundevent, aggregate{fn,field?}, op, threshold, within

A heartbeat — a self-loop firing every 60 Zeqonds, emitting an event other contracts can react to:

{ "from": "running", "to": "running", "operator": "KO42",
"trigger": { "kind": "recurring", "every_zeqonds": 60 },
"post_actions": [ { "type": "emit", "event": "scheduled_tick" } ] }

The scheduler computes next_fire_at_zeqond at deploy and re-arms after every fire. GET .../next-fires predicts the upcoming fires for time-based triggers (event-based triggers can't be predicted without future events — the endpoint says so honestly).

Pre- and post-actions

Each transition can carry up to four pre_actions (before compute) and four post_actions (after the audit row commits):

  • api_call (pre) — call an external API you registered on the machine and bind the response into the transition input: { "type": "api_call", "api": "weather", "path": "/current", "method": "GET", "params": { "q": "London" }, "into": "wx" }. Hard 5-Zeqond timeout; the destination URL is SSRF-guarded before the socket opens.
  • emit (post) — append an event to contract_events (optionally tagged onto a labelled bus) for other contracts' on_event/on_aggregate triggers: { "type": "emit", "event": "over_limit", "payload": {}, "tag": "alerts" }.
  • set_state (post) — override the destination state; last matching when wins: { "type": "set_state", "to": "alert", "when": "result.value > 100" }.

Lifecycle

author ─→ simulate / dry-run ─→ deploy ─→ run ─→ observe ─→ verify
(IDE, (no writes, (audit row, (manual or (Observer, (state-progression
generate, no tokens) v1 snapshot, scheduler) SSE, audit) + integrity checks)
template,
raw JSON)
  1. Author — Contract IDE (/apps/contract-ide/): describe it in Generate (POST /api/contracts/generate drafts a definition against the real schema + registry — it never deploys), pick a Template, or write JSON in Expert. Or skip the IDE entirely and POST the JSON.
  2. SimulatePOST /api/contracts/simulate previews one transition (inline definition or saved contract): condition trace, would-be proof digest, the operator's master_equation_block. Nothing is written, nothing is minted.
  3. Dry-runPOST .../contracts/:id/dry-run plays a synthetic event sequence (or replays your machine's real recorded events via replay_from_zeqond) against the trigger logic and reports what would fire. GET .../preview does the same over the live event window the IDE polls.
  4. DeployPOST /api/chain/:slug/contracts (or /custom from the IDE, or a template's /deploy). The contract row is created at its initial state, v1 is snapshotted into the version history, a creation row lands on your entangled state, and the first fire is scheduled.
  5. Run — drive transitions yourself or let triggers fire them. Each fire lands on the entangled state with the proof digest and the kernel envelope.
  6. VerifyPOST .../verify walks the state progression; GET .../integrity re-hashes the definition. The entangled state's own hash-chain validation is independent — both must pass.

Endpoints

Discovery + authoring (mounted at /api):

MethodPathWhat it does
GET/api/contracts/templateslist the template catalogue (?category= filter)
GET/api/contracts/templates/categoriescategories with counts
GET/api/contracts/templates/:idone template, full definition included
POST/api/contracts/templates/:id/deploydeploy a template to your machine — body { "slug": "<your-machine>" }
POST/api/contracts/generatenatural language → drafted definition ({ slug, query }); preview only, never deploys
POST/api/contracts/simulatedry-run ONE transition; no chain writes, no tally

Per-machine contract engine (mounted at /api/chain/:slug; auth = session or your machine's zsm_ key):

MethodPathWhat it does
POST/contractscreate from a raw definition (accepts {definition:{…}} or the bare object)
POST/contracts/customthe Contract IDE's deploy path — same validation, same result
GET/contractslist this machine's contracts
GET/contracts/:idcontract + its last 20 transitions
PATCH/contracts/:idupdate the definition — snapshots a new version, never rewrites history
POST/contracts/:id/transitionexecute a transition ({ to, input }) — charged in ZEQ
GET/contracts/:id/auditfull transition history; filters from_zeqond / to_zeqond / triggered_by (glob), cursor-paginated
POST/contracts/:id/verifywalk the state progression; reports valid / broken_at
GET/contracts/:id/integritypublic tamper check: stored vs live definition SHA-256
POST/contracts/:id/dry-runsynthetic events (≤200) or replay_from_zeqond against real recorded events
GET/contracts/:id/previewdry-run over the live event window (?window= 1–1000 Zeqonds) + reactor stats
GET/contracts/:id/next-firespredict upcoming time-trigger fires (?count= ≤50)
POST/contracts/:id/pause · /resume · /skip-next · /fire-nowoperational controls
POST/contracts/:id/freezeretire permanently — read-only forever, history preserved
GET/contracts/:id/versions · /versions/:nversion history / one snapshot
POST/contracts/:id/rollbackrestore an old version as a new version ({ target_version_no })
GET/contracts/:id/export · POST /contracts/importportable definition envelope (runtime state never travels)
GET/eventsthe machine's contract_events feed (?event=&since_zeqond=&limit=)
GET/events/stream · /contracts/:id/transitions/streamlive SSE: events / successful fires as they happen

One runnable walkthrough

Deploy a template, fire it, read it back off the entangled state:

# 1. Deploy the force-threshold watchdog onto your machine
curl -sS -X POST https://zeqapi.com/api/contracts/templates/force-threshold-alarm/deploy \
-H "Authorization: Bearer ${ZSM_KEY}" -H "Content-Type: application/json" \
-d '{"slug":"my-machine"}'
# → 201 { "ok": true, "contract": { "id": "…", "currentState": "idle", … } }

# 2. Drive it: idle → watching, then feed a reading that breaches the limit
curl -sS -X POST https://zeqapi.com/api/chain/my-machine/contracts/${CONTRACT_ID}/transition \
-H "Authorization: Bearer ${ZSM_KEY}" -H "Content-Type: application/json" \
-d '{"to":"watching","input":{}}'
curl -sS -X POST https://zeqapi.com/api/chain/my-machine/contracts/${CONTRACT_ID}/transition \
-H "Authorization: Bearer ${ZSM_KEY}" -H "Content-Type: application/json" \
-d '{"to":"alert","input":{"mass_kg":1200,"acceleration_ms2":9.81,"force_limit_n":10000}}'
# → { "ok": true, "new_state": "alert", "previous_state": "watching",
# "proof_digest": "…64hex", "zeqond_number": 2287439213, "tally": { … } }

# 3. Read the audit trail, then verify the progression
curl -sS https://zeqapi.com/api/chain/my-machine/contracts/${CONTRACT_ID}/audit \
-H "Authorization: Bearer ${ZSM_KEY}"
curl -sS -X POST https://zeqapi.com/api/chain/my-machine/contracts/${CONTRACT_ID}/verify \
-H "Authorization: Bearer ${ZSM_KEY}"
# → { "ok": true, "valid": true, "broken_at": null, … }

The same fires are visible without the contract API at all: GET /api/chain/my-machine/explore returns the entangled-state narrative, where each contract-driven row carries contract_context: { contractId, fromState, toState }, operator_id, master_equation_block, proof_digest, and the tally token. That is what the Observer's Contracts section renders.


Where the compute runs

By default execution_target is "zeq" — the operator runs on your machine. Set execution_target: "webhook" (plus webhook_url, optional webhook_secret for HMAC) at create/PATCH time to run each fire's compute on your own server: the engine posts the transition payload to your endpoint and the signed result is written back to your entangled state. The URL and secret are AES-256-GCM encrypted at rest and never returned by the API — reads expose only has_webhook / webhook_has_secret. The contract, its state, and its proof trail stay anchored to your machine.


Honest limits

  • Transitions cost ZEQ. Each driven execution is complexity-priced and charged before the work; an empty wallet returns 402 INSUFFICIENT_BALANCE.
  • Failures are recorded, not hidden. Rejected conditions and compute errors land in contract_transitions with success: false — every attempt is on the trail.
  • Freeze is permanent. A frozen contract never transitions again (423 contract_frozen); the history stays readable. Pause/resume is the reversible control.
  • Bounded debug surfaces. Dry-run: ≤200 synthetic events, replay windows capped at 10,000 real events; preview windows ≤1,000 Zeqonds; next-fires ≤50; audit pages ≤100 rows (cursor Z<zeqond> to continue); skip-next stacks to 100.
  • Emitted events expire. contract_events rows carry a TTL in Zeqonds and are archived by GC — dry-run replay reads both hot and archived tables, but don't treat the event bus as permanent storage. The entangled state is the permanent record.
  • Conditions are deliberately small. No function calls, no loops, no I/O in the grammar. Heavy logic belongs in the operator or in api_call pre-actions.
  • Export is definition-only. Fire counts, watermarks, audit history, and webhook secrets never travel with an exported envelope; an import starts clean on the target machine.

Next

  • Contract IDE — author, preview, deploy, and manage contracts (the Saved tab is their home).
  • Observer — /state/?slug=<your-machine> → Contracts section: every contract and every fire on your entangled state.
  • Contracts API reference · Templates API reference — field-level request/response shapes.
  • Tally tokens — the receipt minted on every successful fire.
  • Operators — the verb on each transition.
  • Seven-step protocol — what KO42 + the operator do inside the engine.
  • State Channels — host the page that fronts your contract on the same machine.
  • Worked example — Thermal router — poll → compute → emit, end to end.