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
kind | Fires when | Key fields |
|---|---|---|
recurring | every N Zeqonds, or on a cron schedule | every_zeqonds or cron (+ cron_tz), max_fires? |
one_shot | once, at a fixed Zeqond | at_zeqond or at_unix |
on_event | an emitted event lands | event, from_slug?, where?, into? |
on_events | several events combine (any / all) | match, events[], within? |
on_logic | a recursive AND/OR tree of events matches | tree, within? |
on_aggregate | an aggregate over a window crosses a bound | event, 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 tocontract_events(optionally tagged onto a labelled bus) for other contracts'on_event/on_aggregatetriggers:{ "type": "emit", "event": "over_limit", "payload": {}, "tag": "alerts" }.set_state(post) — override the destination state; last matchingwhenwins:{ "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)
- Author — Contract IDE (
/apps/contract-ide/): describe it in Generate (POST /api/contracts/generatedrafts 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. - Simulate —
POST /api/contracts/simulatepreviews one transition (inline definition or saved contract): condition trace, would-be proof digest, the operator'smaster_equation_block. Nothing is written, nothing is minted. - Dry-run —
POST .../contracts/:id/dry-runplays a synthetic event sequence (or replays your machine's real recorded events viareplay_from_zeqond) against the trigger logic and reports what would fire.GET .../previewdoes the same over the live event window the IDE polls. - Deploy —
POST /api/chain/:slug/contracts(or/customfrom 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. - Run — drive transitions yourself or let triggers fire them. Each fire lands on the entangled state with the proof digest and the kernel envelope.
- Verify —
POST .../verifywalks the state progression;GET .../integrityre-hashes the definition. The entangled state's own hash-chain validation is independent — both must pass.
Endpoints
Discovery + authoring (mounted at /api):
| Method | Path | What it does |
|---|---|---|
GET | /api/contracts/templates | list the template catalogue (?category= filter) |
GET | /api/contracts/templates/categories | categories with counts |
GET | /api/contracts/templates/:id | one template, full definition included |
POST | /api/contracts/templates/:id/deploy | deploy a template to your machine — body { "slug": "<your-machine>" } |
POST | /api/contracts/generate | natural language → drafted definition ({ slug, query }); preview only, never deploys |
POST | /api/contracts/simulate | dry-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):
| Method | Path | What it does |
|---|---|---|
POST | /contracts | create from a raw definition (accepts {definition:{…}} or the bare object) |
POST | /contracts/custom | the Contract IDE's deploy path — same validation, same result |
GET | /contracts | list this machine's contracts |
GET | /contracts/:id | contract + its last 20 transitions |
PATCH | /contracts/:id | update the definition — snapshots a new version, never rewrites history |
POST | /contracts/:id/transition | execute a transition ({ to, input }) — charged in ZEQ |
GET | /contracts/:id/audit | full transition history; filters from_zeqond / to_zeqond / triggered_by (glob), cursor-paginated |
POST | /contracts/:id/verify | walk the state progression; reports valid / broken_at |
GET | /contracts/:id/integrity | public tamper check: stored vs live definition SHA-256 |
POST | /contracts/:id/dry-run | synthetic events (≤200) or replay_from_zeqond against real recorded events |
GET | /contracts/:id/preview | dry-run over the live event window (?window= 1–1000 Zeqonds) + reactor stats |
GET | /contracts/:id/next-fires | predict upcoming time-trigger fires (?count= ≤50) |
POST | /contracts/:id/pause · /resume · /skip-next · /fire-now | operational controls |
POST | /contracts/:id/freeze | retire permanently — read-only forever, history preserved |
GET | /contracts/:id/versions · /versions/:n | version history / one snapshot |
POST | /contracts/:id/rollback | restore an old version as a new version ({ target_version_no }) |
GET | /contracts/:id/export · POST /contracts/import | portable definition envelope (runtime state never travels) |
GET | /events | the machine's contract_events feed (?event=&since_zeqond=&limit=) |
GET | /events/stream · /contracts/:id/transitions/stream | live 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_transitionswithsuccess: 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_eventsrows 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_callpre-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.