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#
| Credential | How it is sent | What it can do |
|---|---|---|
| Session | the 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 key | Authorization: Bearer cw_live_… | Owns the account's worlds: every route. |
| Actor token | Authorization: Bearer cwa_<world>_… | Only GET …/environments/{env}, …/step, …/observe, …/scene and …/render of the one environment it was issued for. |
| Agent token | Authorization: Bearer cwt_… | Only POST /v1/agent/chat, POST /v1/agent/vision and GET /v1/agent/usage: what the Computatrum app is given by the device sign-in (§ Computatrum). |
- 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/keyslists them,POST /v1/keysmakes 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 oneEnvironment. 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 path | What 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/signout | Ends the session. |
GET /me | The 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=30 | Daily 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 path | What it does |
|---|---|
GET /plans | Public: 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/portal | A Stripe Customer Portal URL: card, invoices, cancel. |
POST /billing/webhook | Stripe's signed events (set in Stripe's dashboard). |
Catalog (no credential needed)
| Method and path | What it does |
|---|---|
GET /health | Liveness 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/vocabulary | Platforms (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 path | What 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}/topology | Its 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 path | What 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}/trajectory | The definition; every action so far. |
GET, PATCH /worlds/{id}/services/{service}/state | A 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}/topology | The 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 path | What 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 path | Who | What it does |
|---|---|---|
GET, POST /worlds/{id}/environments {actor?, machines?, actions?, observations?, action_budget?, label?} | owner | Grants; 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}]} | both | Up 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}/observe | both | The granted observation channels. |
GET …/environments/{env}/scene?width&height&elements=1 | both | The 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|rgba | both | The 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 path | What 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 path | What 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}/fork | Fork the arms now. |
GET, POST /swarms/{id}/journal | The LLM journal; answers for pending decisions. |
GET /swarms/{id}/trace?arm=&since=, GET /swarms/{id}/summary | One 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, 503 model_unavailable (with Retry-After), agent_not_configured or
billing_not_configured, 507 world_too_large (see § Limits of one object). The device
sign-in's polling answers 400 with the OAuth device-grant codes (§ Computatrum).
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#
| Quota | Free | Pro, $29 a month | Enterprise |
|---|---|---|---|
| Worlds running at once (swarm arms count) | 8 | 32 | 128 |
| Environments per world | 16 | 64 | 256 |
| Kept snapshots per world | 16 | 64 | 256 |
| Blueprints | 32 | 128 | 512 |
| Swarms, and people in each | 2, of 32 | 8, of 128 | 32, of 512 |
| API keys | 10 | 25 | 100 |
| Definition size | 4 MiB | 4 MiB | 4 MiB |
| Requests a minute | 600 | 3,000 | 12,000 |
Actions (step) a minute | 1,200 | 6,000 | 24,000 |
| Renders a minute | 120 | 600 | 2,400 |
| Model calls a day (swarms' LLM people) | 500 | 5,000 | 50,000 |
| Computatrum agent-minutes in a rolling 5 hours | 15 | 120 | 1,200 |
| Computatrum agent-minutes in a rolling week | 30 | 300 | 3,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 ([email protected]) 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:
| Event | Effect |
|---|---|
checkout.session.completed | Pro, active; the renewal date from the subscription; the code redeemed. |
customer.subscription.updated | active/trialing → Pro; past_due → past due; unpaid, canceled, incomplete_expired → Free. Renewal and cancel-at-period-end follow. |
customer.subscription.deleted | Free, canceled. |
invoice.payment_failed | past_due: Pro is kept for 7 days (BILLING_GRACE_DAYS), then the account is Free until a payment succeeds. |
checkout.session.expired | The 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"}'
Computatrum#
Computatrum is Computaco's desktop app that works the person's own computer, with their cursor and keyboard, as a computer-use agent. Each step it takes asks two models: a vision model what is on the screen and where, and a chat model what to do next. This API serves both, signs the app in, and meters every call against the account's plan (in agent-minutes), or pay as you go (in credits).
| Route | Credential | What it does |
|---|---|---|
POST /v1/agent/chat | any (agent token included) | OpenAI-style chat completions by DeepSeek deepseek-flash (AGENT_CHAT_MODEL) |
POST /v1/agent/vision | any | {image, prompt, max_tokens?} → {text, usage} by Workers AI @cf/zai-org/glm-5.3-flash (AGENT_VISION_MODEL) |
GET /v1/agent/usage | any | both windows (minutes), pay as you go this month (credits), the last 30 days by model |
POST /v1/agent/device | none | starts a device sign-in |
POST /v1/agent/device/token | none | the app polls it for its agent token |
GET /v1/agent/device/{user_code}, POST …/approve, POST …/deny | console session only | the person approves or declines the app |
GET /v1/agent/tokens, DELETE /v1/agent/tokens/{id} | session or API key | the signed-in apps, and signing one out |
GET, PUT /v1/billing/payg | session or API key | pay-as-you-go settings |
Chat. The body is OpenAI's: messages (1–500 of {role: system|user|assistant|tool,
content}), max_tokens (default 1,024, at most 8,192; more is lowered to 8,192),
temperature, top_p, stop, response_format, tools, tool_choice and
reasoning_effort are passed on; model and anything else are ignored, and stream: true
is refused (the answer comes whole). The answer is OpenAI's shape — {id, object:
"chat.completion", created, model, choices: [{index, message, finish_reason}], usage:
{prompt_tokens, completion_tokens, total_tokens, cached_tokens}} — plus metering:
{cost_micro, credits, billed: "plan"|"payg", ms, minutes, task?, usd?}, what the call was
charged. A request the
model refuses (400, e.g. too long) answers 400 and costs nothing; the model unreachable or
failing answers 503 model_unavailable with Retry-After: 5.
Vision. image is a base64 PNG, JPEG or WebP (a data: URL works too) of at most 4 MiB
decoded; prompt is the question, up to 20,000 characters; max_tokens defaults to 512, at
most 2,048. The answer is {text, model, usage: {prompt_tokens, completion_tokens,
total_tokens, images: 1}, model_ms, metering}. The model is asked with low reasoning effort
and temperature 0.1. Ask it for coordinates in the image's pixels and it gives them (below).
Signing the app in. The device flow (RFC 8628's shape, our error envelope):
curl -s -X POST $API/agent/device -d '{"client_name": "Computatrum on ada-mbp"}'
# → {"device_code": "cwd_…", "user_code": "BCDF-GHJK", "verification_url": "https://computaco.ai/console/#/device?code=BCDF-GHJK",
# "verification_uri": "https://computaco.ai/console/#/device", "expires_in": 900, "interval": 5, "client_name": "…"}
curl -s -X POST $API/agent/device/token -d '{"device_code": "cwd_…"}'
# → 400 {"error": {"code": "authorization_pending"}} every 5 s until the person approves, then once:
# → {"access_token": "cwt_…", "token_type": "bearer", "scope": "agent", "token_id": "agt_…", "user": {"id", "username", "plan"}}
The app opens verification_url; the console (signed in) shows the app's name and the code,
and the person approves or declines it. Polling faster than interval answers slow_down;
a declined sign-in answers access_denied, an expired one (15 minutes) expired_token, a
used or unknown code invalid_grant. A user code is 8 letters from 20 consonants (34 bits),
lives 15 minutes, and is approved only from a console session, never with a key; a device
code is 32 random bytes, stored as its SHA-256 and exchanged once. The agent token is 32
random bytes, stored as its SHA-256, does not expire, and reaches only the three routes
above; the console's Usage page lists the signed-in apps and signs one out
(DELETE /v1/agent/tokens/{id}) at once. Starting a sign-in counts against the address's
ten sign-up and sign-in attempts a minute.
Agent-minutes, credits and the two windows#
A person thinks in time, not tokens, so a plan's windows are counted in agent-minutes: the
wall-clock time Computatrum is working on a task. A developer thinks in cost, so pay as you go
is counted in credits: $0.001 of model cost. Both are in
cloud/src/meter.ts.
A task's minutes. The app sends the task each call belongs to, a UUID per task, in the
header X-Computatrum-Task (or task_id in the body; 1–64 letters, digits, -, _). A call
adds its own duration (from when the Worker receives it to when the model has answered) plus
the gap since the end of the same task's previous call, up to 15 seconds
(TASK_GAP_CAP_MS); a call overlapping the previous one adds only the time past it. The first
call of a task, and a call with no task, adds its own duration only. So time the agent spends
waiting for the person — a confirmation, a credential handed over, a pause because they moved
the mouse — is counted for at most 15 s, however long it lasts.
Why 15 seconds, measured from the event logs of 12 real tasks run by the app's end-to-end
tests against the live models on dev (the four of 2026-10-28 in
target/computatrum-evidence/ev/*-real/, and eight more on dev with this metering, two rounds
of the same four tests): the gap between one step's decision and the next step's screen read is
the agent acting, the screen settling and the next capture, and the 39 such gaps were
0.43–2.22 s (median 0.92 s). The longest the agent can make one on its own is 12.7 s: a
wait of at most 10 s (MAX_WAIT_MS), the 2 s settle cap and about 0.7 s to capture and show
the marker. 15 s is above that, and well below a person reading a card and answering.
What a minute costs, measured (the same eight tasks on dev, 2026-10-28; metering per call
in each read and think event):
| Task | Steps | Wall time | Charged | Credits | Credits a minute |
|---|---|---|---|---|---|
| Loop: order form, one purchase confirmation | 6 | 75.8 s / 55.0 s | 74.9 s / 54.5 s | 3.700 / 3.843 | 2.96 / 4.23 |
| Loop: login, the password handed to the person | 4 / 5 | 24.7 s / 27.0 s | 24.0 s / 26.4 s | 2.278 / 3.007 | 5.70 / 6.83 |
| App: sign in, pause for the mouse, Allow once, done | 6 | 55.6 s / 56.5 s | 55.2 s / 56.0 s | 3.826 / 3.790 | 4.16 / 4.06 |
| App: the stop key mid-task | 1 | 7.3 s / 6.0 s | 6.8 s / 4.7 s | 0.636 / 0.509 | 5.64 / 6.47 |
| All eight | 5.04 min | 21.589 | 4.28 |
A screen read was 0.296–0.335 credits (0.315 on average), a decision 0.173–0.505 (0.311); a
step took 7.77 s at the median. The account's own GET /v1/agent/usage said 2.68 minutes after
the first round, the sum of its calls' ms.
The two windows. A plan has two rolling windows, as consumer AI subscriptions do: a
5-hour window that starts with the first call after the last one ended, and a weekly
window likewise. A call is admitted while both have minutes left and its whole time is
counted after it runs, so a window can end up to one call over. When one is used up, calls
answer 429 quota_exceeded with Retry-After the seconds to its reset (the later reset when
both are) — or go to pay as you go when it is on. The windows were in credits until
2026-10-28; on the change every account's windows started again empty.
The fair-use guardrail. Minutes alone would let a loop that calls the models far faster, or
with far larger prompts, than the app does spend without bound inside its minutes. So a window
is also full once its calls have cost 15 credits for each minute of its limit
(FAIR_USE_CREDITS_PER_MINUTE): 1,800 credits in Pro's 5 hours, 4,500 in its week. The person
sees minutes, not this number; a window full this way says "fair_use": true. The app's own
tasks cost 4.28 credits a minute (6.83 at most, above); the most its loop can cost is a busy
desktop with every prompt at its largest — a 4,000-token decision prompt, 670 tokens of it from
cache, and 300 out (1,363 µ$), a 1440×900 screen read with 200 out (368 µ$), 1.73 credits a
step — at the median 7.77 s a step: 13.4 credits a minute. 15 is above that and 3.5 times
the measured average, so no one using the app reaches it.
GET /v1/agent/usage:
{"plan": "pro", "unit": {"name": "minute", "task_gap_cap_s": 15, "payg": {"name": "credit", "model_cost_usd": 0.001, "price_usd": 0.0025}},
"windows": {"five_hour": {"limit": 120, "used": 31.25, "remaining": 88.75, "started_at": 1793232000000, "resets_at": 1793250000000, "full": false},
"weekly": {"limit": 300, "used": 96.5, "remaining": 203.5, "started_at": 1793001600000, "resets_at": 1793606400000, "full": false}},
"payg": {"enabled": false, "requested": false, "subscribed": false, "status": "none", "mode": "overflow", "monthly_cap_usd": 20,
"period_start": 1790812800000, "period_end": 1793491200000, "credits": 0, "spend_usd": 0, "unreported_credits": 0},
"last_30_days": [{"kind": "chat", "model": "deepseek-flash", "calls": 1290, "tokens_in": 1270000, "tokens_cached": 503000, "tokens_out": 361000, "images": 0, "minutes": 31.4, "credits": 401.2, "payg_credits": 0}, …]}
A call's own metering says what it added: {"cost_micro": 311, "credits": 0.311, "billed":
"plan", "ms": 2140, "minutes": 0.04, "task": "4f1c2a9e-…"}.
The model costs behind a credit are the providers' list prices, always at the peak rate:
| Model | Input | Cached input | Output |
|---|---|---|---|
deepseek-flash (DeepSeek, peak; off-peak is half) | $0.30 / M | $0.006 / M | $1.20 / M |
@cf/zai-org/glm-5.3-flash (Workers AI; an image is input tokens) | $0.15 / M | — | $0.50 / M |
The plans, and the most each can cost: every weekly window of a month used to the last minute, every minute at the fair-use ceiling (52/12 weeks a month):
| Plan | 5-hour window | Weekly window | Most model cost a month | At the measured 4.28 credits a minute |
|---|---|---|---|---|
| Free | 15 min | 30 min | 30 × 15 × $0.001 × 52/12 = $1.95 | $0.56 |
| Pro, $29 | 120 min | 300 min | 300 × 15 × $0.001 × 52/12 = $19.50 | $5.56 |
| Enterprise | 1,200 min | 3,000 min | $195.00 | by agreement |
Why Pro stays profitable. Pro is $29 a month; Stripe keeps 2.9% + $0.30 = $1.14, leaving $27.86. The most a Pro account can cost in models is its weekly window used to the last minute, every week, at the fair-use ceiling: 300 min × 15 credits × $0.001 × 52/12 = $19.50 a month, leaving $8.36 (29%) at the worst possible use, before the off-peak half price DeepSeek gives most hours. At the cost real tasks measured, a fully used Pro costs $5.56 a month and leaves $22.30 (77%). The 5-hour window holds 120 of the week's 300 minutes, so one sitting can use at most two fifths of the week. Free's worst case is $1.95 a month and its measured case $0.56. 120 minutes is about a hundred tasks like the order form (55–76 s each).
Pay as you go#
For developers building their own Computatrum (or anyone past their windows): unchanged, in credits, not minutes — credits billed monthly in arrears through Stripe at $0.0025 a credit, $2.50 per 1,000 — 2.5 times model cost, which pays Stripe's fees and the free plan. It is on the account's own Stripe customer, beside any plan.
PUT /v1/billing/payg {"enabled": true, "monthly_cap_usd": 20, "mode": "overflow"}turns it on. The first time, it answers{"payg": …, "checkout_url": "https://checkout.stripe.com/…"}: a Checkout Session for a subscription to the metered price (STRIPE_PRICE_PAYG, or lookup keycomputatrum_payg_credits), which takes the card; pay as you go is on once Stripe's webhook says the subscription is (payg.subscribed). Turning it off, or changing the cap or mode, is immediate; the subscription stays (nothing is billed without use) and the Customer Portal cancels it.mode: "overflow"(the default) uses the plan's windows first and pay as you go after;"always"bills every call to pay as you go and leaves the windows alone (for an app of your own with your own users).monthly_cap_usd(1–10,000, default 20) is the most it spends in a calendar month (UTC), at its price; past it, calls answer 429 until the month ends or the cap is raised.- Each call billed this way is reported to a Stripe Billing Meter (event
computatrum_credits,payload.stripe_customer_id,payload.valuein whole credits) by the accounts object's alarm, everyAGENT_METER_FLUSH_Sseconds (60): whole credits only, the fraction kept for the next report, each report with anidentifier(cw_<user>_<n>) that Stripe deduplicates, so a retry is billed once. - Its subscription's events carry
metadata.kind = "payg"and never change the plan: created or updatedactiveturns it on,past_due(or a failed PAYG invoice) suspends it until paid, deleted turns it off.
The Stripe objects are made by node cloud/scripts/stripe-agent-setup.mjs <tier>, which
finds or makes the meter, a product and the price on it, and writes STRIPE_PRICE_PAYG and
STRIPE_METER_PAYG into the tier's env file:
| Tier | Meter | Price | Product |
|---|---|---|---|
| local, dev (test mode) | mtr_test_61Vf7N6HWXMyp4NOQ412LVcDG4iLF0lM | price_1UVjPk2LVcDG4iLFrdU9lG4P | prod_VWmz6vg8SGHneG |
| prod (live mode) | mtr_61Vf7N9Oubm4Tu21S412LVcDG4iLFH2O | price_1UVjPn2LVcDG4iLFecAI84kT | prod_VWmz0X1glSo6BJ |
The models, and why#
The chat model is DeepSeek's deepseek-flash (served by DeepSeek-V4.1-Flash), the cheapest
capable model the account already holds a key for. The vision model was chosen from the
Workers AI catalog (wrangler ai models list, 2026-10-28) on the three screenshots above,
each asked two things: to read the screen (events on a day, an email's subject and sender, a
pull request's number, title and the reviewer who asked for changes) and where a control is,
in pixels:
| Model | Read correctly | Control found within 10 px | Latency | Cost of a 960×600 screenshot |
|---|---|---|---|---|
@cf/zai-org/glm-5.3-flash | 3 of 3, every run | 3 of 3, every run (2–7 px) | 1.3–2.9 s | 146 µ$ |
@cf/google/gemma-4-26b-a4b-it (thinking off) | 2 of 3 | 0 of 3 | 1.2–1.8 s | ~45 µ$ |
@cf/meta/llama-4-scout-17b-16e-instruct | 3 of 3 | 0 of 3 | 2.2–9.0 s | ~350 µ$ |
@cf/mistralai/mistral-small-3.1-24b-instruct | 3 of 3 (one name misread) | 0 of 3 | 1.7–2.0 s | ~330 µ$ |
@cf/moondream/moondream3.1-9B-A2B | — answered empty through the REST API | — | — | — |
@cf/cloudflare/clef-omni, Cloudflare's own multimodal decision model, answers typed
questions (yes/no, choice, score) with probabilities rather than text, so it cannot be the
text answer this route promises; it could score a screen ("is the dialog open?") in a
later route. GLM-5.3-flash is the only model that both read and pointed right, at a cost a
step can carry; AGENT_VISION_MODEL changes it without a deploy of code (a model with no
listed rate is charged at a fallback of $1 / $4 per million tokens).
Downloads#
The app's installers are 100+ MB, over the 25 MiB a Pages file may be, so they live in R2
(bucket computaco-downloads; computaco-downloads-dev for the dev tier) and the API Worker
serves them, public and unmetered, through the Pages Function at /download/*:
curl -s https://computaco.ai/download/computatrum/latest.json
curl -LO https://computaco.ai/download/computatrum/computatrum-0.1.0-linux-arm64.AppImage
latest.json is {version, channel, published, files: [{platform, arch, kind, name, bytes,
sha256, url}]} (platform is linux, mac or windows; kind is AppImage, deb,
tar.gz, dmg, zip or exe), cached for 60 s; an installer is cached for an hour. Both
answer HEAD, one Range (206, or 416 past the end), If-Range and If-None-Match.
.github/workflows/computatrum.yml builds and uploads them on a computatrum-v* tag
(computatrum/scripts/publish-downloads.mjs writes the manifest and uploads it last).
How it is built#
computaco.ai ───────────── /, /docs/, /console/ static files (Pages; its pages.dev names 308 to computaco.ai)
└─ /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
SwarmObjectcoordinates the rounds, the fork, the journal and the summary. - Staging.
npm run enginecopies the engine and the catalog (everyworlds/*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):
| host | Worker | application modules | script, gzipped (wrangler) |
|---|---|---|---|
core | computerworld (the API itself) | KiCad, Python | 9,048 KiB |
office | computerworld-office | Spreadsheet, Excel, Database; VS Code | 9,509 KiB |
media | computerworld-media | Clipchamp, iMovie, Kdenlive, Video Editor; Music; Maps | 9,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 (asProcedure::preparedoes 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;
hostnames 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/runon a world whose host lacks one of the procedure's modules is a409naming the host to fork to; on a world booted without the procedure's desktops, a409saying 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.
| world | domains that run | host(s) | Wasm memory after the run | what does not fit |
|---|---|---|---|---|
pro-workstation | cad (6/6), eda (5/5), office (4/4), media (3/3) | media (CAD), core (KiCad), office | 9–78 MB | — |
company-2026 | gov (3/4), ops (4/4), biz (2/2) | office | 99–121 MB | swe (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-co | cad, eda, fin, mkt, ops, sales, swe (14/14) | office, core | 50–97 MB | — |
lantern-media | med (2/3) | media | 57–64 MB | med-video-claim-dispute: rendering in Kdenlive grows the engine to 169 MB and the video module to 13 MB |
agent-desktop | 2/2 | office | 7–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-up | 415 ms |
boot agent-desktop (a new object: its isolate, the engine, the fonts) | 1,721 ms |
| step (actor token) | 55 ms |
| scene | 84 ms |
| render 1280×800 PNG | 986 ms |
| snapshot / restore / export | 124 / 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 |
| step | 27.5 ms |
| scene | 21.9 ms |
| render 1280×800 + PNG (render 91–169 ms, encode 27–32 ms in the object) | 155 ms |
| snapshot | 32 ms |
| fork | 62 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, and the dev tier's project answers at dev.computaco.ai.
cloud/pages/functions/_middleware.ts sends every other name a deployment answers to (www,
and the projects' own *.pages.dev names and preview URLs) to its tier's one host with a
308, method kept, so a session cookie only ever lives on one host and nothing is reachable
at a name we do not publish.
There are three tiers, always named (cloud/scripts/tiers.mjs), and no bare deploy:
| Tier | Where | Workers | Pages project | Stripe | Secrets |
|---|---|---|---|---|---|
| local | wrangler dev, http://localhost:8787 | — | — | test mode | cloud/.env.local → .dev.vars |
| dev | https://dev.computaco.ai | computerworld-dev, -office-dev, -media-dev | computerworld-dev | test mode | cloud/.env.dev |
| prod | https://computaco.ai | computerworld, -office, -media | computerworld | live mode | cloud/.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 dev.computaco.ai
npm run deploy:prod # the same for prod, e2e against computaco.ai
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.