Dory is in development. The invite-only beta has not opened yet; prices and limits are planned and may change.

Quickstart: your first computer

Preview of the launch flow. Sign-up and packages are not available yet.

In about fifteen minutes, plus the time your Bitcoin payment takes to confirm, you will create a computer, run a command, read a file, look at its desktop, click and type, open a web page it serves, pause it, resume it and delete it. The compute in this guide costs less than one cent of your credit.

You need:

  • a shell with curl and jq (macOS, Linux or WSL) for the curl tab, Python 3.10 or later for the Python tab (on Debian/Ubuntu, install python3-venv), or Node 18 or later for the TypeScript tab;
  • a Bitcoin wallet for your first top-up ($20 minimum).

1. Sign up

  1. Open https://agents.computeengines.com and choose Sign up.
  2. Enter your email, a password and, during the beta, your invite code.
  3. Choose your country and whether you sign up as a business or developer, then accept the terms and policies (these will be published before the beta opens).
  4. Complete required two-factor enrollment (password plus authenticator, or a user-verified passkey); we strongly recommend enrolling a second authenticator, because launch has no password or account recovery.

Your account starts at Tier 0: it can pay for credit but cannot create computers yet.

2. Top up

  1. In the console, open Billing → Top up, enter $20 (the on-chain minimum) and choose Bitcoin.
  2. The console tells you that checkout runs on our Bitcoin checkout, then sends you there. Pay the invoice from your wallet before it expires (60 minutes).
  3. Your credit appears once the payment has one confirmation on the Bitcoin network (two or more for amounts above $250). That commonly takes tens of minutes. The top-up's status moves from new to processing to credited.
  4. After the first credited top-up we send a verification email. Click the link. Your account is now Tier 1: up to 2 running computers, sizes ac-2 and ac-4.

Later top-ups can be started from code: POST /v1/billing/topups returns a checkout_url. An agent can create that link; a person pays it.

3. Create an API key and install a client

  1. In the console, open API keys → Create key. Name it quickstart, pick the Full preset and, as a safety net, set an expiry of 7 days and a maximum running cost of $0.10 an hour.
  2. Confirm your password and existing authenticator, or verify your passkey, when asked; this check must be less than five minutes old. Copy the key (cek_live_…). It is shown once.
  3. In the same local shell from which you will run any client, set the key using the common prerequisite below. This block is outside the language tabs. Never put this key in the remote computer or a committed file.
read -rs CE_API_KEY && export CE_API_KEY

Keep all curl snippets in one Bash session, all Python snippets in one quickstart.py file executed with python quickstart.py, and all JavaScript snippets in one quickstart.mjs file executed with node quickstart.mjs; execute them in order. Do not run individual Python/JavaScript snippets in fresh processes.

curl

export API=https://api.agents.computeengines.com/v1
set -euo pipefail
ID=''
cleanup_notice() {
  local rc=$?
  if [ "$rc" -ne 0 ] && [ -n "${ID:-}" ]; then
    printf 'Stopped with computer %s retained; it may still be running and billed. Inspect it in the console. Stop it there, or delete it with: cec rm %s --yes\n' "$ID" "$ID" >&2
  fi
}
trap cleanup_notice EXIT
ce_request() (
  local out='' method=GET body headers code rc line delay remaining
  local deadline=${CE_DEADLINE:-$((SECONDS + 120))}
  local -a args=()
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -o) out="$2"; shift 2 ;;
      -X) method="$2"; args+=("$1" "$2"); shift 2 ;;
      *) args+=("$1"); shift ;;
    esac
  done
  body=$(mktemp) || exit 1
  headers=$(mktemp) || { rm -f "$body"; exit 1; }
  trap 'rm -f "$body" "$headers"' EXIT
  while :; do
    remaining=$((deadline - SECONDS))
    if [ "$remaining" -le 0 ]; then
      printf 'Request deadline exceeded; no success assumed.\n' >&2
      exit 1
    fi
    if [ "$remaining" -gt 65 ]; then remaining=65; fi
    : > "$headers"
    if code=$(curl -sS --connect-timeout 10 --max-time "$remaining" \
      -D "$headers" -o "$body" -w '%{http_code}' "${args[@]}"); then
      :
    else
      rc=$?
      printf 'Transport failure (%s); request is not retried because its outcome may be unknown.\n' "$rc" >&2
      exit "$rc"
    fi
    case "$code" in
      2??)
        if [ -n "$out" ]; then cat "$body" > "$out"; else cat "$body"; fi
        exit 0 ;;
    esac
    if [ "$method" = GET ] && { [ "$code" = 429 ] || [ "$code" = 503 ]; }; then
      delay=5
      while IFS= read -r line; do
        line=${line%$'\r'}
        case "$line" in
          [Rr][Ee][Tt][Rr][Yy]-[Aa][Ff][Tt][Ee][Rr]:*)
            line=${line#*:}
            line=${line//[[:space:]]/}
            if [[ "$line" =~ ^[0-9]{1,6}$ ]]; then delay=$((10#$line)); fi ;;
        esac
      done < "$headers"
      if [ "$delay" -lt 1 ]; then delay=1; fi
      if [ $((SECONDS + delay)) -lt "$deadline" ]; then sleep "$delay"; continue; fi
    fi
    printf 'HTTP %s; stopping. Error body:\n' "$code" >&2
    cat "$body" >&2
    printf '\n' >&2
    exit 1
  done
)
ce_wait_computer() {
  local initial="$1" expected="$2" need_ready="$3"
  local cid op status current
  local CE_DEADLINE=$((SECONDS + 600))
  cid=$(printf '%s' "$initial" | jq -er '.id // .computer.id // .operation.computer_id | select(type == "string" and length > 0)') || return 1
  op=$(printf '%s' "$initial" | jq -r '.operation.id // empty') || return 1
  if [ -n "$op" ]; then
    while :; do
      if [ "$SECONDS" -ge "$CE_DEADLINE" ]; then
        printf 'Wait timed out: computer %s, operation %s; operation may still be running.\n' "$cid" "$op" >&2
        return 1
      fi
      current=$(ce_request "$API/operations/$op" -H "Authorization: Bearer $CE_API_KEY") || return 1
      status=$(printf '%s' "$current" | jq -er '.state | select(. == "pending" or . == "running" or . == "succeeded" or . == "failed")') || return 1
      case "$status" in
        succeeded) break ;;
        failed)
          printf '%s' "$current" | jq '.error' >&2
          printf 'Operation failed for computer %s; it may still be running and billed.\n' "$cid" >&2
          return 1 ;;
        pending|running) sleep 5 ;;
      esac
    done
  fi
  # Successful delete ends in a tombstone: GET Computer would return 410.
  if [ "$expected" = deleted ]; then
    [ -n "$op" ] || printf '%s' "$initial" | jq -e '(.computer // .).state == "deleted"' >/dev/null || return 1
    if [ -n "$op" ]; then printf '%s\n' "$current"; else printf '%s\n' "$initial"; fi
    return 0
  fi
  current=$(ce_request "$API/computers/$cid" -H "Authorization: Bearer $CE_API_KEY") || return 1
  printf '%s' "$current" | jq -e --arg state "$expected" --argjson ready "$need_ready" \
    'select(.state == $state and (($ready == false) or .ready == true))'
}
ce_request "$API/me" -H "Authorization: Bearer $CE_API_KEY" | jq '{id, tier}'

Python

python3 -m venv .venv
. .venv/bin/activate
python -m pip install computeengines   # available when the beta opens
from computeengines import Client

client = Client()                      # reads CE_API_KEY from the environment
print(client.billing.balance())

TypeScript

npm install @computeengines/agent-computers   # available when the beta opens
// quickstart.mjs (also valid TypeScript); run with: node quickstart.mjs
import { writeFile } from "node:fs/promises";
import { Client } from "@computeengines/agent-computers";

const client = new Client();           // reads CE_API_KEY from the environment
console.log(await client.billing.balance());

The SDKs and the CLI exchange your key for short-lived access tokens whenever they talk to a computer. The curl tab shows that step explicitly in step 5.

4. Create a computer

curl

IDEM=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)   # keep it; reuse it if you retry
ce_request -X POST "$API/computers?wait=60" \
  -H "Authorization: Bearer $CE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEM" \
  -d '{"size": "ac-2", "name": "quickstart"}' > computer.json
ID=$(jq -er '.id // .computer.id // .operation.computer_id | select(type == "string" and length > 0)' computer.json)
echo "computer $ID"

CREATED=$(ce_wait_computer "$(cat computer.json)" running true)
printf '%s\n' "$CREATED" | jq '{id,state,ready}'

Python

computer = client.computers.create(size="ac-2", name="quickstart")
print(computer.id, computer.state)     # c4f7k2x9q1a8z running

TypeScript

const computer = await client.computers.create({ size: "ac-2", name: "quickstart" });
console.log(computer.id, computer.state);

Idempotency-Key is required when you create a computer, so a retried request never creates two. The SDKs add one for you and wait until the computer is ready (its in-guest agent answers). Creating a computer boots a whole virtual machine; the create target is p95 at or below 120 seconds; see Honest limits.

If you supplied setup, readiness does not mean it succeeded: poll GET /setup before depending on its changes. SDK and CLI lifecycle calls use the bounded waiting and error rules in the Python SDK, TypeScript SDK and CLI reference; pause completes before returning and resume returns only when ready. A waiter timeout does not cancel a computer or stop billing.

The computer is Debian-based with an XFCE desktop, Chromium, Docker, Python, Node and SSH. You work as the user dev (home /home/dev), who can use sudo. Unless you say otherwise it pauses after 1 hour (timeout_seconds).

5. Run a command

curl

# Mint an access token for this computer (valid 60 minutes here; 15 by default)
ACCESS=$(ce_request -X POST "$API/computers/$ID/access" \
  -H "Authorization: Bearer $CE_API_KEY" -H "Content-Type: application/json" \
  -d '{"expires_in": 3600}')
TOKEN=$(printf '%s' "$ACCESS" | jq -er '.token | select(type == "string" and length > 0)')
DATA=$(printf '%s' "$ACCESS" | jq -er '.data_url | select(type == "string" and length > 0)')

ce_request -X POST "$DATA/exec" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"command": "uname -sr; python3 -V; echo hello from CE > /home/dev/hello.txt"}' | jq

# Start a small web server in the background (used in steps 8 and 9)
ce_request -X POST "$DATA/processes" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"command": "python3 -m http.server 8000 --directory /home/dev", "timeout_seconds": 3600}' | jq
{"proc_id": "proc_…", "exit_code": 0, "signal": null, "timed_out": false,
 "stdout": "Linux …\nPython 3.…\n", "stderr": "", "truncated": false, "duration_ms": 52}

Python

r = computer.exec("uname -sr; python3 -V; echo hello from CE > /home/dev/hello.txt")
print(r.exit_code, r.stdout)

server = computer.processes.start("python3 -m http.server 8000 --directory /home/dev", timeout_seconds=3600)

TypeScript

const r = await computer.exec("uname -sr; python3 -V; echo hello from CE > /home/dev/hello.txt");
if (!("exit_code" in r)) throw new Error("Exec replay; retrieve retained output before continuing");
console.log(r.exit_code, r.stdout);

const server = await computer.processes.start("python3 -m http.server 8000 --directory /home/dev", { timeout_seconds: 3600 });

command runs through bash -lc; use argv to skip the shell. A synchronous exec has a 60-second process deadline by default (600 seconds maximum). processes.start (REST POST /processes) starts background work with no process deadline when omitted; this example explicitly sets 3600 seconds. Finish the walkthrough within that hour: the process deadline, the computer run timer, and access-token expiry are separate limits. For longer work, start a background process and read output later, or use Streaming exec with its documented timeout. Background processes survive HTTP disconnect and token expiry, not stop/reboot; computer lifecycle and balance limits still apply. An explicit process deadline terminates the process group.

Do not blindly retry a command that may have run. For safe retries, pass your own key: curl -H "Idempotency-Key: $EXEC_KEY", Python computer.exec(command, idempotency_key=exec_key), TypeScript computer.exec(command, {idempotency_key: execKey}), or cec exec <id> --idempotency-key <key> -- <argv...>. Choose and retain one unique key per intended command, and reuse that key with the same payload only for retries of that command. The same option applies to processes.start and streaming exec (exec_stream in Python, execStream in TypeScript); SDKs do not generate data-plane keys automatically. See Idempotency and retries. A replay returns process metadata rather than the original exit/output result; use proc_id to fetch retained output before proceeding. The examples above omit a key and do not automatically retry an ambiguous failure.

Before opening Chromium, wait for the sample HTTP server to answer inside the computer. Run this bounded command through the same exec interface and require exit_code=0. It does not connect to localhost on your developer machine.

curl

SERVER_CHECK="python3 -c \"import time
import urllib.error
import urllib.request
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
    try:
        with urllib.request.urlopen('http://127.0.0.1:8000/hello.txt', timeout=2) as response:
            if response.status == 200 and response.read().strip() == b'hello from CE':
                break
    except (OSError, urllib.error.URLError):
        pass
    time.sleep
else:
    raise SystemExit('Sample HTTP server did not become ready within 60 seconds')\""
jq -n --arg command "$SERVER_CHECK" '{command:$command,timeout_seconds:70}' |
  ce_request -X POST "$DATA/exec" -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" -d @- | jq -e '.exit_code == 0'

Python

import shlex

SERVER_CHECK = "python3 -c " + shlex.quote("""import time
import urllib.error
import urllib.request
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
    try:
        with urllib.request.urlopen('http://127.0.0.1:8000/hello.txt', timeout=2) as response:
            if response.status == 200 and response.read().strip() == b'hello from CE':
                break
    except (OSError, urllib.error.URLError):
        pass
    time.sleep
else:
    raise SystemExit('Sample HTTP server did not become ready within 60 seconds')""")
check = computer.exec(SERVER_CHECK, timeout_seconds=70)
if check.exit_code != 0:
    raise RuntimeError(f"Server readiness failed for {computer.id}; inspect it in the console")

TypeScript

const SERVER_CHECK = `python3 -c "import time
import urllib.error
import urllib.request
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
    try:
        with urllib.request.urlopen('http://127.0.0.1:8000/hello.txt', timeout=2) as response:
            if response.status == 200 and response.read().strip() == b'hello from CE':
                break
    except (OSError, urllib.error.URLError):
        pass
    time.sleep
else:
    raise SystemExit('Sample HTTP server did not become ready within 60 seconds')"`;
const check = await computer.exec(SERVER_CHECK, { timeout_seconds: 70 });
if (!("exit_code" in check)) throw new Error("Exec replay; retrieve retained output before continuing");
if (check.exit_code !== 0) throw new Error(`Server readiness failed for ${computer.id}; inspect it in the console`);

6. Read a file

curl

ce_request "$DATA/files?path=/home/dev/hello.txt" -H "Authorization: Bearer $TOKEN"
# hello from CE

# Writing works the same way, with the raw file as the body:
ce_request -X PUT "$DATA/files?path=/home/dev/notes.txt" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/octet-stream" --data-binary 'first note'

Python

print(computer.files.read("/home/dev/hello.txt").decode())    # hello from CE
computer.files.write("/home/dev/notes.txt", b"first note")

TypeScript

console.log(new TextDecoder().decode(await computer.files.read("/home/dev/hello.txt")));
await computer.files.write("/home/dev/notes.txt", new TextEncoder().encode("first note"));

Paths must be absolute. One request moves up to 100 MB.

7. Take a screenshot

curl

# Open Chromium on the desktop at the server you started
ce_request -X POST "$DATA/desktop/launch" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"url": "http://localhost:8000/"}'
sleep 3
ce_request "$DATA/desktop/screenshot?format=png" -H "Authorization: Bearer $TOKEN" -o screen.png

Python

import time

computer.desktop.launch(url="http://localhost:8000/")
time.sleep(3)
with open("screen.png", "wb") as f:
    f.write(computer.desktop.screenshot(format="png"))

TypeScript

await computer.desktop.launch({ url: "http://localhost:8000/" });
await new Promise((ok) => setTimeout(ok, 3000));
await writeFile("screen.png", await computer.desktop.screenshot({ format: "png" }));

Open screen.png and verify Chromium has loaded the /home/dev directory listing from localhost:8000. The preceding sleep does not establish readiness. If the browser is not visible or the page is loading, take another screenshot after one second; stop after 60 seconds rather than continuing into input actions. If it shows an HTTP error, check the server and reload before continuing. Screenshots are PNG, JPEG or WebP and can be scaled or cropped; CE-Scale-X/CE-Scale-Y report display pixels per image pixel (scale=0.5 gives 2.0), region-relative when cropped, just like JSON scale_x/scale_y. Multiply image coordinates by the factors.

8. Click and type

Coordinates are always display pixels. Only after that visual check, focus the visible Chromium window before sending the navigation keys. The center click below assumes Chromium occupies the screen center; if it does not, focus it in the viewer first or choose the Chromium window using GET /desktop/windows and POST /desktop/windows/{wid}/focus.

curl

DISPLAY_JSON=$(ce_request "$DATA/desktop/display" -H "Authorization: Bearer $TOKEN")
X=$(echo "$DISPLAY_JSON" | jq '.width / 2 | floor'); Y=$(echo "$DISPLAY_JSON" | jq '.height / 2 | floor')

jq -n --argjson x "$X" --argjson y "$Y" '{actions: [
    {type: "click", x: $x, y: $y},
    {type: "key",   keys: "ctrl+l"},
    {type: "type",  text: "localhost:8000/hello.txt"},
    {type: "key",   keys: "Return"},
    {type: "wait",  ms: 1500}
  ]}' |
ce_request -X POST "$DATA/desktop/actions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @-

ce_request "$DATA/desktop/screenshot?format=png" -H "Authorization: Bearer $TOKEN" -o after.png

Python

d = computer.desktop.display()
computer.desktop.click(d.width // 2, d.height // 2)
computer.desktop.key("ctrl+l")
computer.desktop.type("localhost:8000/hello.txt")
computer.desktop.key("Return")
time.sleep
with open("after.png", "wb") as f:
    f.write(computer.desktop.screenshot(format="png"))

TypeScript

const d = await computer.desktop.display();
await computer.desktop.actions([
  { type: "click", x: Math.floor(d.width / 2), y: Math.floor(d.height / 2) },
  { type: "key", keys: "ctrl+l" },
  { type: "type", text: "localhost:8000/hello.txt" },
  { type: "key", keys: "Return" },
  { type: "wait", ms: 1500 },
]);
await writeFile("after.png", await computer.desktop.screenshot({ format: "png" }));

Open after.png and verify the page shows "hello from CE". If it is still loading, repeat only the screenshot (not the typing/click batch), once per second for up to 60 seconds; otherwise stop and troubleshoot instead of claiming success. One actions call takes up to 20 actions: click (left, right, middle, back, forward; single, double, triple; with modifier keys), move, drag along a path, mouse down/up, scroll, type, key combinations, key down/up, hold a key, and wait. Scroll units are wheel notches only; positive dx is right and positive dy is down. Enter and Return are aliases. To watch, open a view-only viewer link (cec desktop <id> --view). For an exclusive handoff, follow Human takeover; a non-exclusive control link does not block agent actions. cec desktop <id> --exclusive requests an exclusive link. Open and connect it before expecting human_in_control protection; minting the link alone does not take control.

9. Open a preview URL

Previews give a port on your computer a private HTTPS address.

curl

PREVIEW=$(ce_request -X POST "$API/computers/$ID/previews" \
  -H "Authorization: Bearer $CE_API_KEY" -H "Content-Type: application/json" \
  -d '{"port": 8000, "expires_in": 3600}')
echo "$PREVIEW" | jq '{id, url, access_url, access_url_expires_at, expires_at}'

# For a program instead of a browser, create a preview token and send it as a header:
PRV=$(printf '%s' "$PREVIEW" | jq -er '.id | select(type == "string" and length > 0)')
URL=$(printf '%s' "$PREVIEW" | jq -er '.url | select(type == "string" and length > 0)')
CPT=$(ce_request -X POST "$API/computers/$ID/previews/$PRV/tokens" \
  -H "Authorization: Bearer $CE_API_KEY" -H "Content-Type: application/json" \
  -d '{"expires_in": 3600}' | jq -er '.token | select(type == "string" and length > 0)')
ce_request "$URL/hello.txt" -H "CE-Preview-Token: $CPT"

Python

preview = computer.previews.create(port=8000, expires_in=3600)
print(preview.url)           # https://p8000-c4f7k2x9q1a8z.<preview-domain>
print(preview.access_url)    # open this one in your browser

TypeScript

const preview = await computer.previews.create({ port: 8000, expires_in: 3600 });
console.log(preview.url, preview.access_url);

Open access_url in your browser before access_url_expires_at (the earlier of ten minutes after issuance or preview expiry). It works once: redemption sets a cookie for that preview host and redirects to its clean url. The browser grant expires at the earliest of 24 hours after redemption, preview expiry or originating-link expiry; it does not last until the preview expires if the link expires first. Reloads and assets using the cookie do not consume another use. To show the page to someone else, create a share link (Tier 1: up to 24 hours and 10 uses). Explicit link revocation invalidates redeemed grants within 15 seconds; exhausting uses alone does not. Delete the preview to revoke its links, grants and tokens within 15 seconds.

10. Pause and resume

Pausing writes the computer's memory to disk. Every window and process, including your web server, is still there after resume.

curl

PAUSE=$(ce_request -X POST "$API/computers/$ID/pause?wait=60" -H "Authorization: Bearer $CE_API_KEY")
PAUSED=$(ce_wait_computer "$PAUSE" paused false)
printf '%s\n' "$PAUSED" | jq '.state'
RESUME=$(ce_request -X POST "$API/computers/$ID/resume?wait=60&wait_for=ready" -H "Authorization: Bearer $CE_API_KEY")
RESUMED=$(ce_wait_computer "$RESUME" running true)
printf '%s\n' "$RESUMED" | jq '{state,ready}'
ce_request "$DATA/ports" -H "Authorization: Bearer $TOKEN" | jq

Python

computer.pause()
computer.resume()
print(computer.ports())      # 8000 is still listening

TypeScript

await computer.pause();
await computer.resume();
console.log(await computer.ports());

While paused, the computer is not billed for compute; its disk and saved memory are stored at $0.05 per GB-month inside its monthly cap. Calls to a paused computer's data API return 409 invalid_state. If ?wait runs out, the API returns 202 with an operation. The Bash helper checks HTTP errors, polls the operation and validates the final Computer before proceeding; its local 600-second deadline does not cancel the server operation. On timeout/failure, inspect retained computer/operation IDs before retrying; the script never blindly repeats a mutation. A resume can return 503 capacity_unavailable with Retry-After when capacity is in use; try again later, or use an always-on computer with a retained slot. A slot released for low balance is not restored automatically; unreserved always-on/own-storage charges remain paused until explicit capacity admission or a switch to on-demand. Account-storage charges resume when retention clears.

11. Delete

curl

DELETE=$(ce_request -X DELETE "$API/computers/$ID?wait=60" \
  -H "Authorization: Bearer $CE_API_KEY" -H "CE-Confirm-Delete: $ID")
DELETED=$(ce_wait_computer "$DELETE" deleted false)
printf '%s\n' "$DELETED" | jq '.state'
ID=''
ce_request "$API/billing/balance" -H "Authorization: Bearer $CE_API_KEY" | jq

On earlier failure the Bash script prints the retained computer ID and stops. Inspect it in the console and stop/delete it as appropriate; readiness failure or a local timeout does not stop billing. If the initial create response was lost, reuse the same create Idempotency-Key rather than issuing a fresh create.

Python

computer.delete(confirm=computer.id)
print(client.billing.balance())

TypeScript

await computer.delete({ confirm: computer.id });
console.log(await client.billing.balance());

The CE-Confirm-Delete header must repeat the computer id. On accepted deletion, billing stops at once, every access token for the computer stops working, and its files are purged after 24 hours. For 30 days the API answers 410 gone for it. Delete completes when the computer is off and quarantined; the physical purge happens later and is not another operation success. The curl helper waits for operation completion and prints succeeded (polled operation) or deleted (immediate Computer response). SDK and CLI delete return on acceptance or no-op success, not operation completion or physical purge. If a legal hold prevents deletion, the API returns 409 invalid_state with retryable:false instead of accepting it; that refusal does not stop billing. Stop or pause remains available where the lifecycle permits, and held data is not purged on the ordinary schedule.

About 15 minutes of an ac-2 costs 900 s × $0.000008 = $0.0072.

The same with the CLI

Prerequisites: Bash, jq, and uv or pipx. This walkthrough creates its own files; it does not require the curl tab or its variables.

set -euo pipefail
uv tool install computeengines   # available when the beta opens; or: pipx install computeengines
cec login                                       # prompts for the key without echoing it
ID=$(cec create --size ac-2 --name quickstart --json | jq -r .id)   # waits until ready
cec exec "$ID" -- bash -lc 'uname -sr; echo hello from CE > /home/dev/hello.txt'
cec cp "$ID":/home/dev/hello.txt .
cec exec "$ID" --background --timeout-seconds 3600 -- python3 -m http.server 8000 --directory /home/dev
SERVER_CHECK="import time
import urllib.error
import urllib.request
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
    try:
        with urllib.request.urlopen('http://127.0.0.1:8000/hello.txt', timeout=2) as response:
            if response.status == 200 and response.read().strip() == b'hello from CE':
                break
    except (OSError, urllib.error.URLError):
        pass
    time.sleep
else:
    raise SystemExit('Sample HTTP server did not become ready within 60 seconds')"
cec exec "$ID" --timeout-seconds 70 -- python3 -c "$SERVER_CHECK"
cec desktop launch "$ID" --url http://localhost:8000/
cec screenshot "$ID" -o screen.png
# Verify screen.png shows the localhost:8000 directory listing and Chromium is focused
# before continuing; use the bounded screenshot/retry procedure in steps 7-8.
DISPLAY_JSON=$(cec desktop display "$ID" --json)
X=$(printf '%s' "$DISPLAY_JSON" | jq -er '.width | select(type == "number" and . > 0) | . / 2 | floor')
Y=$(printf '%s' "$DISPLAY_JSON" | jq -er '.height | select(type == "number" and . > 0) | . / 2 | floor')
jq -n --argjson x "$X" --argjson y "$Y" '{actions: [
  {type: "click", x: $x, y: $y},
  {type: "key", keys: "ctrl+l"},
  {type: "type", text: "localhost:8000/hello.txt"},
  {type: "key", keys: "Return"},
  {type: "wait", ms: 1500}
]}' > actions.json
cec desktop act "$ID" --json actions.json
cec screenshot "$ID" -o after.png
# Verify hello from CE is visible; repeat only screenshots within the step-8 deadline.
cec preview "$ID" 8000
cec pause "$ID" && cec resume "$ID"
cec rm "$ID" --yes

If something goes wrong

You see Meaning Do this
402 insufficient_balance Your balance does not cover at least $1 or one hour of your projected spend Top up (step 2)
403 email_unverified or 403 tier_limit Still Tier 0, or that size needs a higher tier Verify your email after your first credited top-up; use ac-2 or ac-4 on Tier 1
403 key_cost_limit The key's hourly cost limit would be exceeded Use a smaller size or raise the limit on the key
409 invalid_state The current state or a legal hold prevents this action Read detail, current_state and allowed_actions; a legal hold blocks destructive actions, not stop/pause. Do not blindly retry
503 capacity_unavailable / 503 host_busy No room right now, or the platform is limiting new starts Wait for Retry-After, then retry
502 agent_unreachable The computer's in-guest agent did not answer See Troubleshooting

Next: Use with AI agents · Lifecycle and states · Pricing and billing · Honest limits.

Dory is in development; these pages describe the planned service and may change. Questions: support@computeengines.com.