Computaco

Start

The hosted API#

ComputerWorld runs as a hosted service at https://computaco.ai/v1. You sign up, make an API key, and drive worlds over HTTP: boot one from a built-in world or your own definition, grant an agent an environment, send it pointer and keyboard actions, read its screen as a scene or a PNG, snapshot and fork it, and run and verify expert procedures and societies. Every world is the same deterministic engine as the Python wheel and the npm package, so a run over HTTP replays to the same state hashes.

The contract is cloud/openapi.yaml (also served at https://computaco.ai/openapi.yaml); this page is the guide to it. The web console at /console/ is built on the same routes.

Quickstart#

Sign up (the answer carries your first API key, shown once), then boot a world and grant an environment:

API=https://computaco.ai/v1
curl -s $API/auth/signup -H 'content-type: application/json' \
  -d '{"username":"ada","password":"a long enough password"}'
# → {"user": {...}, "api_key": {"id": "key_…", "key": "cw_live_…", …}}
export CW_KEY=cw_live_…

curl -s $API/worlds -H "authorization: Bearer $CW_KEY" \
  -d '{"world":"agent-desktop","seed":1}'
# → {"id": "w_…", "state_hash": "…", "tick": 0, "machines": ["workstation"], …}
W=w_…

curl -s $API/worlds/$W/environments -H "authorization: Bearer $CW_KEY" -d '{}'
# → {"id": "env_1", "token": "cwa_w_…", "config": {"actor": "ada", "machines": ["workstation"],
#     "actions": ["pointer.v1", "keyboard.v1", "application.v1"], …}}

Hand the actor token to the agent. It can read the screen, act, and look:

T=cwa_w_…
curl -s "$API/worlds/$W/environments/env_1/scene?elements=1" -H "authorization: Bearer $T" \
  | jq '.elements[] | select(.interaction == "shell:launch:editor") | .center'
# → [34, 187]
curl -s $API/worlds/$W/environments/env_1/step -H "authorization: Bearer $T" -d '{"actions": [
  {"family": "pointer.v1", "op": "click", "payload": {"x": 34, "y": 187, "width": 1280, "height": 800}},
  {"family": "keyboard.v1", "op": "type", "payload": {"text": "Hello"}}]}'
curl -s -o screen.png "$API/worlds/$W/environments/env_1/render?width=1280&height=800" \
  -H "authorization: Bearer $T"

The Python and JavaScript clients wrap all of this, with element finding, retries and errors that carry the API's code:

from computerworld_cloud import Client   # pip install "computerworld-cloud @ git+https://github.com/ComputaCo/computerworld.git#subdirectory=python"

with Client("cw_live_…").create_world("agent-desktop", seed=1) as world:
    env = world.environment()
    env.click(interaction="shell:launch:editor")
    env.render().save("screen.png")

The same calls by hand:

Python#

import requests

API = "https://computaco.ai/v1"
owner = requests.Session()
owner.headers["authorization"] = "Bearer cw_live_…"

world = owner.post(f"{API}/worlds", json={"world": "agent-desktop", "seed": 1}).json()
env = owner.post(f"{API}/worlds/{world['id']}/environments", json={}).json()

agent = requests.Session()
agent.headers["authorization"] = f"Bearer {env['token']}"
base = f"{API}/worlds/{world['id']}/environments/{env['id']}"
scene = agent.get(f"{base}/scene", params={"elements": 1}).json()
x, y = next(e["center"] for e in scene["elements"] if e["interaction"] == "shell:launch:editor")
result = agent.post(f"{base}/step", json={"actions": [
    {"family": "pointer.v1", "op": "click", "payload": {"x": x, "y": y, "width": 1280, "height": 800}},
]}).json()
print(result["outcomes"][0]["success"], result["state_hash"])
open("screen.png", "wb").write(agent.get(f"{base}/render").content)

# The owner forks, and runs an expert procedure on the fork.
fork = owner.post(f"{API}/worlds/{world['id']}/fork", json={}).json()
run = owner.post(f"{API}/worlds/{fork['id']}/procedures/run",
                 json={"ref": {"world": "agent-desktop", "id": "legal-engagement"}, "seed": 2}).json()
print(run["verification"]["passed"], run["demonstration"]["step_count"])

Send a User-Agent of your own. Cloudflare, in front of the API, refuses Python's bare urllib default (Python-urllib/3.x) with a 403 whose body is error code: 1010, before the request reaches ComputerWorld; requests, httpx, curl and browsers are not affected. With urllib, set one: urllib.request.Request(url, headers={"User-Agent": "my-agent/1.0", ...}).

JavaScript#

const API = 'https://computaco.ai/v1';
const owner = (path, body) => fetch(API + path, {
  method: body ? 'POST' : 'GET',
  headers: { authorization: `Bearer ${process.env.CW_KEY}`, 'content-type': 'application/json' },
  body: body && JSON.stringify(body),
}).then((r) => r.json());

const world = await owner('/worlds', { world: 'agent-desktop', seed: 1 });
const env = await owner(`/worlds/${world.id}/environments`, {});
const step = await fetch(`${API}/worlds/${world.id}/environments/${env.id}/step`, {
  method: 'POST',
  headers: { authorization: `Bearer ${env.token}`, 'content-type': 'application/json' },
  body: JSON.stringify({ actions: [{ family: 'keyboard.v1', op: 'key', payload: { key: 'Ctrl+Alt+t' } }] }),
}).then((r) => r.json());
console.log(step.state_hash);

Authentication#

CredentialHow it is sentWhat it can do
Sessionthe cw_session cookie that sign-up and sign-in set (HttpOnly, SameSite=Lax, Secure over HTTPS, 30 days)Everything an API key can, from the console's origin. A write sent with a cookie must carry an Origin of the same host.
API keyAuthorization: Bearer cw_live_…Owns the account's worlds: every route.
Actor tokenAuthorization: Bearer cwa_<world>_…Only GET …/environments/{env}, …/step, …/observe, …/scene and …/render of the one environment it was issued for.
  • Accounts are a username (3–32 of a-z 0-9 _ -) and a password (10–200 characters), stored as PBKDF2-HMAC-SHA-256 with a 16-byte random salt and 100,000 iterations, the most workerd's WebCrypto allows. Unknown usernames are hashed too, so timing does not say which exist. Five wrong passwords for one username lock it for the rest of a 15-minute window, and each address may try sign-up and sign-in ten times a minute.
  • API keys are shown once, when made; the service keeps their SHA-256 and a 12-character prefix. GET /v1/keys lists them, POST /v1/keys makes one, DELETE /v1/keys/{id} revokes it at once.
  • Actor tokens are the HTTP form of the engine's owner/actor split: the API key holds the World, the token holds one Environment. A token is stored as its hash inside its world's object; revoking the environment, resetting the world or deleting it ends it, and a fork issues new tokens for its copies of the environments.

Endpoints#

All paths are under /v1. Bodies and answers are JSON unless noted. Every answer that changes a world carries state_hash and tick (the world clock, logical microseconds).

Accounts

Method and pathWhat it does
POST /auth/signup {username, password, referral_code?}Make an account; sets the session and returns the first API key. A referral code is kept on the account and applied when it upgrades; one that is unknown, inactive, expired or used up is a 400 invalid_code.
POST /auth/signin {username, password}Sets the session.
POST /auth/signoutEnds the session.
GET /meThe account, its plan's quotas, its usage this minute, today and in total, and billing: plan, status (none, active, past_due, canceled), renewal, discount, the sign-up referral code (§ Plans and billing).
PATCH /me {name?, email?, date_of_birth?}Set or clear the account's profile; a field left out is kept, null clears it. A name is at most 100 characters, an email needs an @ and a dotted domain, a date of birth is a real past date written yyyy-mm-dd (1900 or later). Anything else is a 400.
GET /me/usage?days=30Daily requests, steps, renders, worlds created and model calls.
POST /me/password {current, next}Change the password; other sessions end.
DELETE /me {password} or {confirm}Delete the account and everything it owns — its swarms and their arms, its worlds with their snapshots and actor tokens, its blueprints, keys, sessions and usage — at once and for good; the username is free again. From a session the body carries the current password; with an API key, confirm (the account's username) or the password. A missing or wrong confirm is a 400, a wrong password a 401.
GET /keys, POST /keys {name}, DELETE /keys/{id}API keys.

Plans and billing — see § Plans and billing below.

Method and pathWhat it does
GET /plansPublic: Free, Pro and Enterprise with their prices and quotas, and billing_configured.
POST /billing/checkout {plan: "pro", referral_code?}A Stripe Checkout URL for the Pro subscription, with the code's discount.
POST /billing/portalA Stripe Customer Portal URL: card, invoices, cancel.
POST /billing/webhookStripe's signed events (set in Stripe's dashboard).

Catalog (no credential needed)

Method and pathWhat it does
GET /healthLiveness and the engine build (engine.build, version, wasm_sha256).
GET /catalog/worlds, GET /catalog/worlds/{name}The built-in worlds (each with boots: whether this deployment's engine boots it, and the engine's reason if not), and one's whole definition.
GET /catalog/procedures[?world=], GET /catalog/procedures/{world}/{id}Expert procedures.
GET /catalog/societies, GET /catalog/societies/{world}/{id}Society specs.
GET /catalog/vocabularyPlatforms (and platforms_available, what this engine draws), presentations, zones, service kinds, action families, observation channels.

Blueprints and topology — an editable world definition kept by the account.

Method and pathWhat it does
GET /blueprints, POST /blueprints {name, from | definition}List; make one from a built-in world, another blueprint, or a definition.
GET, PUT {name?, definition?}, DELETE /blueprints/{id}One blueprint.
GET /blueprints/{id}/topologyIts machines (profile, user, address, node, theme, presentation as a word, installed_apps, desktop_apps, packages, the initial_files paths), nodes, links (with id, their index), services, DNS and themes.
PATCH /blueprints/{id}/topology {edits: [...]}Apply edits in order, validate, save — all or nothing; a failure names the edit.
POST /validate {definition}Boot it with the engine and say whether it is a world.

The edits: add_machine {computer, node?, links?, theme?}, update_machine {id, set}, remove_machine {id} (with its node's links when nothing else is on the node), add_node, remove_node, add_link {link: {from, to, bidirectional?, latency_us?, loss_per_million?}}, update_link {id, set}, remove_link {id}, add_service {service}, update_service {id, set}, remove_service {id}, set_theme {machine, theme}, set_presentation {machine, presentation}, set_applications {machine, applications}, set_internet, set_implicit_lan, set_gateway {gateway}, set_metadata {key, value}. The fields are the world schema's.

Worlds (owner)

Method and pathWhat it does
GET /worlds, POST /worlds {world | blueprint | definition, seed?, platform?, name?, procedure?, host?}List; boot one. procedure ({world, id} or inline) boots it for that procedure: its machines get the desktops the procedure names and the world goes to a host carrying its applications (§ Hosts).
GET, DELETE /worlds/{id}Summary (with boot_ms and memory.wasm_bytes); delete with its snapshots and tokens.
GET /worlds/{id}/state_hash{state_hash, tick}.
POST /worlds/{id}/advance {ticks | duration}Move the clock ("30s", "2h", "1d"): timers, settlement windows and queues fire.
POST /worlds/{id}/reset {seed?}Back to the definition; environments are dropped.
GET /worlds/{id}/inspect[?pointer=]The owner's view of the whole state, or one JSON Pointer into it.
GET /worlds/{id}/definition, GET /worlds/{id}/trajectoryThe definition; every action so far.
GET, PATCH /worlds/{id}/services/{service}/stateA service's state; an owner write (merge patch), recorded for replay.
POST /worlds/{id}/perform {environment, gesture}One grounded gesture ({do: "click", target: {label: "Post"}}), as procedures use.
GET /worlds/{id}/topologyThe running world's topology (live: true).
POST /worlds/{id}/machines {computer, node?, links?}, DELETE /worlds/{id}/machines/{m}Add or remove a machine live. Links, services and themes of a running world are fixed at boot: edit a blueprint and boot it.

Snapshots, forks, export

Method and pathWhat it does
GET, POST /worlds/{id}/snapshots {label?}Kept snapshots.
POST /worlds/{id}/snapshots/{s}/restore, DELETE /worlds/{id}/snapshots/{s}Restore (environments granted later are dropped), drop.
POST /worlds/{id}/fork {snapshot?, name?, host?}A new world from now or a snapshot, at the same state hash, with the environments carried over under new tokens; host moves the copy to another host.
GET /worlds/{id}/export, POST /worlds/import {snapshot}A portable bundle (definition, seed, the engine's snapshot, environments) and back.

Environments

Method and pathWhoWhat it does
GET, POST /worlds/{id}/environments {actor?, machines?, actions?, observations?, action_budget?, label?}ownerGrants; a new one returns its token once. Defaults: the first machine and its user, pointer.v1, keyboard.v1, application.v1, semantic.v1, a budget of 100,000 actions.
GET, DELETE /worlds/{id}/environments/{env}owner (GET: actor too)One grant; revoke it.
POST …/environments/{env}/step {actions: [{family, op, machine?, payload}]}bothUp to 64 actions. machine may be left out when the grant has one. An action outside the grant is an unsuccessful outcome, not an HTTP error.
GET …/environments/{env}/observebothThe granted observation channels.
GET …/environments/{env}/scene?width&height&elements=1bothThe scene graph; elements adds every interactive node with its role, label, interaction id, clipped rect and center, front first.
GET …/environments/{env}/render?width&height&format=png|rgbabothThe screen of the environment's focused machine; headers X-State-Hash, X-Tick, ETag. Send If-None-Match to poll: an unchanged screen is a 304, not drawn and not counted.

Action families and payloads are the engine's.

Procedures (expert procedures)

Method and pathWhat it does
POST /procedures/instantiate {procedure | ref: {world, id}, seed, overrides?}Draw an instance.
POST /worlds/{id}/procedures/stage {instance}Write its setup files.
POST /worlds/{id}/procedures/project {instance, environment, options?}Perform it through an environment (or {role: env}) and return the demonstration.
POST /worlds/{id}/procedures/verify {instance, captures?}Its checks against the world; captures (a demonstration's) fills checks that name a captured {{@name}}.
POST /worlds/{id}/procedures/run {procedure | ref, seed, fork?, include_steps?, options?}All four; grants its own sessions. fork: true runs in a throwaway copy and leaves the world as it was. The world must be on a host carrying the applications the procedure launches and have its desktops: a 409 says which host, or to boot a world with procedure.

Swarms (societies) — see § Swarms below.

Method and pathWhat it does
POST /swarms/compile {spec | ref, world?, seed?}The compiled plan: people, devices, accounts, follows, arms, what was left uncompiled.
GET /swarms, POST /swarms {spec | ref, world?, seed?, name?, journal?}List; compile and boot (round 0). journal is "builtin" (the journal recorded beside a built-in society), a journal file's text, or [{key, reply}].
GET, DELETE /swarms/{id}State: round, arms with their worlds, hashes and metrics; delete with the arms' worlds.
POST /swarms/{id}/rounds {count?, ask_model?}Run rounds in every arm; the matched fork happens at fork_at.
POST /swarms/{id}/forkFork the arms now.
GET, POST /swarms/{id}/journalThe LLM journal; answers for pending decisions.
GET /swarms/{id}/trace?arm=&since=, GET /swarms/{id}/summaryOne arm's trace (JSON lines); outcomes, checks, divergence and the CRN check.

Swarms#

A swarm is a society run over the API. The spec is compiled in the Worker by cloud/src/society/, a TypeScript port of scripts/society/ that gives the same compiled people and follows and the same state hashes as the Python runner on the same engine, and replays the recorded rumor-cascade journal with no miss. A swarm boots a prefix arm; at fork_at each arm of the design becomes a world of its own (the prefix exported, imported, the runner's state copied, the arm's ops applied), and every round runs in every arm together. Arms count against the world quota.

People whose policy is llm ask a model. Their replies are the journal, keyed by the prompt's bytes, so a run with a journal replays exactly. A round that meets a prompt the journal lacks is rolled back in that arm and comes back as pending (the key, the person and the prompt); answer them with POST /swarms/{id}/journal and run the round again, or send ask_model: true and the deployment asks its model (DeepSeek, deepseek-flash) and reruns the round itself, counting each call against model_calls_per_day.

curl -s $API/swarms -H "authorization: Bearer $CW_KEY" \
  -d '{"ref": {"world": "company-2026", "id": "rumor-cascade"}, "journal": "builtin"}'
curl -s $API/swarms/$S/rounds -H "authorization: Bearer $CW_KEY" -d '{"count": 2}'
curl -s $API/swarms/$S/summary -H "authorization: Bearer $CW_KEY"

Errors and limits#

An error is {"error": {"code", "message"}}: 400 bad_request, 401 unauthenticated, 403 forbidden, 404 not_found (also another account's world), 409 conflict, 413 too_large, 422 invalid_world / invalid_edit / invalid_procedure, 429 rate_limited or quota_exceeded with Retry-After, 500 engine_error, 502 model_error, 507 world_too_large (see § Limits of one object).

Each account's quotas are its plan's (GET /v1/me shows them, GET /v1/plans lists every plan's); see § Plans and billing.

An actor token's work counts against its world's owner; it is limited in its world's object at Free's per-minute rates (the object does not know its owner's plan) and added to the account's counts after it is answered.

Plans and billing#

QuotaFreePro, $29 a monthEnterprise
Worlds running at once (swarm arms count)832128
Environments per world1664256
Kept snapshots per world1664256
Blueprints32128512
Swarms, and people in each2, of 328, of 12832, of 512
API keys1025100
Definition size4 MiB4 MiB4 MiB
Requests a minute6003,00012,000
Actions (step) a minute1,2006,00024,000
Renders a minute1206002,400
Model calls a day (swarms' LLM people)5005,00050,000

Free is the deployment's QUOTA_* vars (cloud/wrangler.jsonc); Pro and Enterprise are in cloud/src/quotas.ts and never fall below Free. The definition size is the same on every plan because a world is one Durable Object, whose isolate has 128 MB on every plan (§ Limits of one object): there is no larger memory class to sell. Enterprise is by agreement (sales@computaco.ai) and set by the operator, not sold here.

Each limit is enforced from the account's plan where it was before: requests, steps and renders a minute when a request is authorized; API keys, blueprints, swarms and model calls in the objects that hold them; a world's environments and snapshots in the Worker before it forwards the grant or snapshot (the world object itself holds up to the largest plan's). What an account already holds when it drops to a smaller plan is kept; it cannot make more until it is under the limit.

Upgrading. POST /v1/billing/checkout {"plan": "pro", "referral_code": "FIRST100"} answers a Stripe Checkout URL (a subscription, monthly, for the price STRIPE_PRICE_PRO, or the price with lookup key computaco_pro_monthly, which it makes on first use). The account stays Free until Stripe tells the webhook it paid. Success and cancel return to /console/#/settings?billing=success and …=cancel. POST /v1/billing/portal answers a Customer Portal URL to change the card, see invoices or cancel (at the period's end).

The webhook (POST /v1/billing/webhook) checks Stripe-Signature — HMAC-SHA256 of <t>.<payload> under STRIPE_WEBHOOK_SECRET, t within 300 s — and handles each event id once:

EventEffect
checkout.session.completedPro, active; the renewal date from the subscription; the code redeemed.
customer.subscription.updatedactive/trialing → Pro; past_due → past due; unpaid, canceled, incomplete_expired → Free. Renewal and cancel-at-period-end follow.
customer.subscription.deletedFree, canceled.
invoice.payment_failedpast_due: Pro is kept for 7 days (BILLING_GRACE_DAYS), then the account is Free until a payment succeeds.
checkout.session.expiredThe code's reserved place is given back.

Without STRIPE_SECRET_KEY (and, for the webhook, STRIPE_WEBHOOK_SECRET) every billing route answers 503 billing_not_configured, GET /v1/plans says billing_configured: false, and the pricing page and console say upgrades open soon. No account is shown as paid that Stripe has not charged.

Referral and discount codes. A code has a percentage off, the plan it is for, a duration (forever — for the life of the subscription —, once, or repeating for duration_in_months), an optional max_redemptions and expires_at, and an active flag. An account names one at sign-up (referral_code, kept on the account) or at checkout. Checkout reserves one of the code's places for the session's hour, in the one accounts object without awaiting between the check and the write, so the place after the last is refused (the tests run 101 checkouts at once against 100 places: 100 sessions, one invalid_code); payment redeems it, an expired session gives it back. An account redeems a code once. The discount is a Stripe coupon cw_<CODE> made from the code on its first checkout. The launch code is FIRST100: 20% off Pro, forever, for the first 100 customers.

The operator's code routes sit beside the account routes (§ Deploying), behind ADMIN_TOKEN:

curl -s $API/admin/codes -H "authorization: Bearer $ADMIN_TOKEN" -d '{"code": "FIRST100",
  "percent_off": 20, "plan": "pro", "duration": "forever", "max_redemptions": 100}'   # 201
curl -s $API/admin/codes -H "authorization: Bearer $ADMIN_TOKEN"                    # {codes: [...]}, with remaining
curl -s $API/admin/codes/FIRST100 -H "authorization: Bearer $ADMIN_TOKEN"           # the code and each account's reservation or redemption
curl -s -X POST $API/admin/codes/FIRST100/deactivate -H "authorization: Bearer $ADMIN_TOKEN"
curl -s -X PATCH $API/admin/users/ada -H "authorization: Bearer $ADMIN_TOKEN" -d '{"plan": "enterprise"}'

How it is built#

computaco.ai ───────────── /, /docs/, /console/  static files (Pages; computerworld.pages.dev is the same project)
                        └─ /v1/*  Pages Function ──service binding──▶ Worker `computerworld`
                                                                       ├─ Accounts (one object): users, sessions, keys, per-minute counters
                                                                       ├─ Owner (one per account): its worlds, blueprints, swarms
                                                                       ├─ WorldObject (one per world): the engine's World in memory,
                                                                       │    its definition, last state, snapshots and environments in SQLite
                                                                       └─ SwarmObject (one per swarm): the plan and the arms' worlds
  • One world, one object. A world is a value in an isolate's memory, so each lives in its own Durable Object, which every request about it reaches. The object writes the world's exported state to its SQLite storage at most 1.5 s after the last change (and at once on a snapshot, a restore or a topology change); an object evicted or restarted rebuilds the world from the definition and that state, at the same hash, and reconnects its environments. A change in the last 1.5 s before a crash of the object is lost.
  • The engine in a Worker. workerd will not compile WebAssembly from bytes it fetches, so the lazy build is used: its engine (all five platforms and FreeCAD, 2.71 MB gzipped) and every code module it can link (the service kinds, the HTML engine, JavaScript, SQLite, the snapshot code, the renderer) are bundled into the script as compiled modules, 7.64 MB gzipped in all. What is only data — the render bundle, fonts, the internet's world — is served from the Worker's static assets and read into the module host before the engine asks for it (fonts and the internet at boot, the render bundle before the first frame). FreeCAD is compiled into the engine (CW_WASM_KEEP=freecad); the other heavy applications' modules do not all fit beside it, so they are spread over three Workers, the hosts.
  • Swarms. Each arm's runner lives in its world's object beside the world; a SwarmObject coordinates the rounds, the fork, the journal and the summary.
  • Staging. npm run engine copies the engine and the catalog (every worlds/* world, procedure, society and recorded journal) and boots each catalog world with that engine, marking those that do not boot. Content moves faster than the engine: build both from the same tree before a deploy.

Hosts#

A Worker's script is limited to 10 MB gzipped, and the engine with its code modules is 7.64 MB of it, so the seven application modules (6.0 MB) cannot all ride beside it. Each host is a Worker carrying the same engine and code modules and some of the application modules, and exporting the same WorldObject class; the API Worker reaches the other hosts' objects through Durable Object bindings to their scripts (cloud/hosts.json, cloud/src/hosts.ts):

hostWorkerapplication modulesscript, gzipped (wrangler)
corecomputerworld (the API itself)KiCad, Python9,048 KiB
officecomputerworld-officeSpreadsheet, Excel, Database; VS Code9,509 KiB
mediacomputerworld-mediaClipchamp, iMovie, Kdenlive, Video Editor; Music; Maps9,185 KiB

FreeCAD is in the engine itself (CW_WASM_KEEP=freecad, +0.51 MB gzipped): a procedure's clicks on a model and its cad_matches checks need the CAD kernel in the engine, so every host runs CAD.

The engine calls a module synchronously, in the world's own isolate, so a world and every module it runs share one host. A world's host is chosen when it boots and is part of its id: w_<hex> on core, w_<hex>-office, w_<hex>-media; an actor token carries the id, so any request reaches the right object without a lookup.

  • POST /v1/worlds {"world": ..., "procedure": {"world": ..., "id": ...}} boots a world for a procedure: on a host carrying every application module its phases launch, and with the desktops the procedure names (as Procedure::prepare does before a world boots).
  • With no procedure, the host carrying most of what the world's machines can launch is chosen, core on a tie; host names one.
  • POST /v1/worlds/{id}/fork {"host": "media"} moves a copy to another host at the same state hash (export, then import there: every host runs the same engine).
  • procedures/run on a world whose host lacks one of the procedure's modules is a 409 naming the host to fork to; on a world booted without the procedure's desktops, a 409 saying to boot one for it.

The catalog says where everything goes: GET /v1/catalog/worlds gives each world's app_modules and default host, GET /v1/catalog/procedures each procedure's app_modules and host, and GET /v1/catalog/vocabulary the hosts and what each carries. A world's summary and a run's answer carry host and memory (the engine's Wasm memory and each application module's instances).

Measured deployed (2026-10-28), each catalog procedure run with procedures/run in a world booted for it (POST /v1/worlds {world, procedure}): 46 of 52 pass, against 2 of the 15 domain procedures tried before the hosts. Wasm memory is the engine's after the run, which never shrinks; the limit is the isolate's 128 MB, with JavaScript beside it.

worlddomains that runhost(s)Wasm memory after the runwhat does not fit
pro-workstationcad (6/6), eda (5/5), office (4/4), media (3/3)media (CAD), core (KiCad), office9–78 MB—
company-2026gov (3/4), ops (4/4), biz (2/2)office99–121 MBswe (0/4): VS Code on the company world grows the engine to 135 MB; gov-late-sales-tax to 120 MB, and the isolate is reset
robotics-cocad, eda, fin, mkt, ops, sales, swe (14/14)office, core50–97 MB—
lantern-mediamed (2/3)media57–64 MBmed-video-claim-dispute: rendering in Kdenlive grows the engine to 169 MB and the video module to 13 MB
agent-desktop2/2office7–10 MB—

The sizes in that column are the engine's own; run in Node from the same build, one world per process, the failures peak at 120.2 MB (gov-late-sales-tax), 134.9 MB and 2.4 MB of VS Code (swe-atlas-clock-tests) and 168.6 MB and 13.1 MB of video (med-video-claim-dispute), so no host split makes them fit: they need the container below. A company world holds 40.6 MB in Node right after it boots and 76.5 MB in its object, which writes the world's first snapshot as it boots.

Limits of one object#

A Durable Object's isolate has 128 MB. agent-desktop holds 6.7 MB of Wasm memory; the internet world alone 42 MB; northstar-company-2026 76 MB in production (85 MB locally), with a 12 MB snapshot. Worlds of that size boot, act, render, snapshot and fork (a fork of the company world took 8.4 s end to end), but little more fits beside them: a society on an internet world (the test town's prefix arm held 93.5 MB after booting nine people) runs out of memory in its first round and answers 507 world_too_large. Locally (wrangler dev, no memory limit) the same swarms run, rumor-cascade included.

The way past it is the one commandagi's serverless devices took: a container. A Cloudflare Container (or Cloud Run) running Node with the same engine and the same object logic holds GBs; the Worker keeps accounts, keys and routing and sends a world marked large to it, with the same affinity rule (a world lives in one process; a request for it reaches that process). That is not built here. A smaller step would compile a society onto only the internet sites it uses instead of joining the whole internet, which changes its state hashes against the Python runner.

Measured#

Deployed (2026-10-28, client in the US, through computerworld.pages.dev's Function and the service binding), node cloud/scripts/e2e.mjs --base https://computerworld.pages.dev, client side:

p50
health (cold Worker)452 ms
sign-up415 ms
boot agent-desktop (a new object: its isolate, the engine, the fonts)1,721 ms
step (actor token)55 ms
scene84 ms
render 1280×800 PNG986 ms
snapshot / restore / export124 / 177 / 100 ms
fork (export, new object, import)1,635 ms
procedure run (legal-engagement, 34 GUI actions, fork)600 ms

A new world object reads 7 MB of fonts and data from static assets before the engine asks (0.4–1.1 s of its start, measured) and compiles the bundled engine on its first request; a resident world answers in tens of milliseconds. Booting company-2026 took 5.9 s.

On this development machine (aarch64, Node 26, wrangler dev, workerd 2026-10-23), client-side over loopback, from node cloud/scripts/e2e.mjs:

p50
sign-up (PBKDF2, 100,000 iterations)32 ms
boot agent-desktop (new object, new world)68 ms
step27.5 ms
scene21.9 ms
render 1280×800 + PNG (render 91–169 ms, encode 27–32 ms in the object)155 ms
snapshot32 ms
fork62 ms
procedure run (legal-engagement, 34 GUI actions)97 ms

Memory and start, in the object: the lazy engine instantiates in 28 ms; agent-desktop boots in 12–13 ms and holds 6.4 MB of Wasm memory, 29 MB after its first frame (the renderer's files and caches); northstar-company-2026 (the internet joined, 19 service kinds) boots in 142 ms and holds 85 MB, and its snapshot is 12.4 MB, exported in 141 ms. rumor-cascade (16 people, 3 arms) replays its recorded journal at about 12 s a round for the three arms.

performance.now() inside a deployed Worker only advances across I/O, so the *_ms fields the objects report (boot_ms, elapsed_ms, X-Render-Ms) read 0 there; measure from the client.

Running it locally#

cd cloud
npm install
# the hosted engine: CW_WASM_LAZY=1 CW_WASM_KEEP=freecad bash scripts/build-wasm.sh,
# pkg/web copied to target/cloud-engine/all/ (binaryen 121 or later for its code modules)
npm run engine           # stages engine/ (a bundle per host), public/_engine, public/_catalog,
                         # and each other host's wrangler.<host>.gen.json
npx wrangler dev --var WORLD_HOSTS:core   # the API alone, http://127.0.0.1:8787/v1
npx wrangler dev -c wrangler.jsonc -c wrangler.office.gen.json -c wrangler.media.gen.json
                         # the API and every host, bound to each other as when deployed
npm test                 # vitest: units, the API on the local runtime, a restart from storage
CW_CLOUD_HOSTS=1 npx vitest run test/hosts.test.ts
                         # a procedure of every domain, each on the host chosen for it
node scripts/e2e.mjs     # the end-to-end journey, with latencies

npm run engine -- --from <dir> stages another build; --engine full stages pkg/web (the whole engine) for local work only: 15.9 MB gzipped, over the limit. A .dev.vars with DEEPSEEK_API_KEY= gives local swarms the model.

Deploying#

The API is the Worker computerworld (its workers.dev route is off: it is reached only through the Pages project), and the site, the docs and the console are the Pages project computerworld, whose /v1/* Function forwards to the Worker through a service binding so the console's cookie is first-party. The project's custom domains are computaco.ai and www.computaco.ai; cloud/pages/functions/_middleware.ts sends the second to the first with a 301, so a session cookie only ever lives on one host. computerworld.pages.dev answers the same deployment and is the name to verify a deploy against, since it does not depend on DNS.

There are three tiers, always named (cloud/scripts/tiers.mjs), and no bare deploy:

TierWhereWorkersPages projectStripeSecrets
localwrangler dev, http://localhost:8787——test modecloud/.env.local → .dev.vars
devhttps://dev.computaco.aicomputerworld-dev, -office-dev, -media-devcomputerworld-devtest modecloud/.env.dev
prodhttps://computaco.aicomputerworld, -office, -mediacomputerworldlive modecloud/.env.prod

The env files are gitignored; cloud/.env.example is their layout. Dev and prod share nothing but the account: their own objects, accounts, rate-limit namespace and Stripe mode. Dev answers with x-robots-tag: noindex.

# the engine, from the same tree as worlds/: CW_WASM_LAZY=1 CW_WASM_KEEP=freecad bash
# scripts/build-wasm.sh, pkg/web copied to target/cloud-engine/all/
cd cloud && npm run engine -- --from ../target/cloud-engine/all
npm run deploy:dev           # hosts, API, secrets, Pages, then e2e against computerworld-dev.pages.dev
npm run deploy:prod          # the same for prod, e2e against computerworld.pages.dev
npm run env:prod             # what a secrets push would set (dry run); -- --apply to set it
npm run dev                  # local: writes .dev.vars from .env.local, then wrangler dev

deploy.sh deploys the world hosts before the API Worker, which binds to them. A tier's first deploy cannot: the hosts bind to the API Worker's AccountStore as well, so the script first puts up the API Worker without the hosts' bindings (wrangler.<tier>.first.gen.json), then the hosts, then the API Worker in full.

e2e.mjs and cloud/console/dev/check-console.mjs delete the accounts they make (DELETE /v1/me), also when a check fails. For accounts left over anyway, the Worker has two operator routes, present only when the secret ADMIN_TOKEN is set (wrangler secret put ADMIN_TOKEN) and a 404 to any request without it:

curl -s "$API/admin/users?prefix=e2e-" -H "authorization: Bearer $ADMIN_TOKEN"   # {users: [{id, username, created_at}]}
curl -s -X DELETE "$API/admin/users/e2e-…" -H "authorization: Bearer $ADMIN_TOKEN" # 204: as DELETE /v1/me

Payment runs on the Stripe account of CommandAGI INC., COMPUTACO INC.'s parent, in test mode for local and dev and live mode for prod. Each mode has the product Computaco Pro, its $29/month price (lookup key computaco_pro_monthly), a Customer Portal configuration of its own (the account's default is CommandAGI's), and per deployed tier a webhook endpoint <origin>/v1/billing/webhook for checkout.session.completed, checkout.session.expired, customer.subscription.updated, customer.subscription.deleted and invoice.payment_failed. Their ids are in each tier's env file as STRIPE_PRICE_PRO, STRIPE_PORTAL_CONFIGURATION and STRIPE_WEBHOOK_SECRET. Nothing reaches localhost, so locally stripe listen --forward-to localhost:8787/v1/billing/webhook stands in for the endpoint.

It needs a Workers Paid plan (each script is 9.3–9.7 MB gzipped; the free plan's limit is 3 MB) for SQLite-backed Durable Objects, the one secret above for swarms' model calls, and the site's own bundle built (site/pkg). Pages refuses files over 25 MiB and the site's full Wasm bundle is 41 MB, so build-pages.sh splits it into 20 MiB parts and the Function at /pkg/* joins them back into one response. The Worker's migrations keep the two classes the placeholder Worker was created with (v1, v2) and replace them in v3.