Skip to main content
DocumentationAliax Interceptor SDK
py 1.0.5 · node 1.0.5

Production Reliability and Deterministic Execution for Visual AI Agents

One SDK, three quickstarts. Use Aliax standalone with raw Playwright, on a headless cloud browser (Browserbase / Steel), or as a 10-line per-step hook for Browser-Use, Stagehand, and LangChain. CLS-immune deterministic execution, zero ghost clicks, 90% DOM token savings, and Sentry-style visual crash replays.

I'm using
pip install aliax
Python 3.8 – 3.12 · async / httpx
Get started

Introduction

Aliax is the layer between your AI model and the live browser that stops ghost clicks and silent stalls. The Steady Hand stamps a unique ID onto every clickable element before acting, so clicks always land on the right target even when the page reflows or re-renders. The Compactor strips the page down to just the interactive elements — about 1,500 tokens instead of 100,000 — and paints a numbered screenshot your model can read. The Airbag sends a visual replay to your dashboard the moment an agent stalls — like Sentry, but for agents.

The Compactor

parse_ui(page) reads the live page and returns only what matters: buttons, inputs, links — each numbered with a bounding box and label. Plus a screenshot with numbered boxes painted over it, ready to hand straight to your model. 90% fewer tokens, every step.

The Steady Hand

execute(page, {action, element_id}) finds the target element by its stamped ID, scrolls it into view, handles React event listeners and iframe offsets, and never throws. Supports 18 actions: CLICK, TYPE, TYPE_AND_ENTER, HOVER, PRESS, SCROLL variants, NAVIGATE, WAIT, DONE, COMBO, BATCH_TYPE, REPORT_ISSUE.

The Airbag

Visual agents have no equivalent of Sentry. report_issue(...) sends the failed screenshot, the element list, and the agent's last reasoning to your dashboard as a visual replay — but only after confirming the agent is genuinely stuck, so a runaway loop can't rack up charges.

Built for pages that don't cooperate
Popups, ads, single-page apps, and layout shifts break most web agents. Aliax solves this at the root: parse_ui stamps each element with a unique ID right before acting, so a layout shift can never move the target out from under the click. No coordinate guessing. No ghost clicks.
Get started

Quickstart

Five steps. You'll have a VLM agent clicking the right element in under five minutes. Code blocks below follow your language pick above — change it any time and every snippet updates in place.

Language
1
Install the SDK
pip install aliax
playwright install chromium
2
Authentication: Zero-signup or Personal Key

No API key required to start: If you omit the key, the SDK automatically bootstraps a free machine-scoped sandbox credential (500 free parses on home/office networks, 100 on cloud egress) cached in ~/.aliax/credentials.

When you're ready for the web dashboard, visual crash telemetry, or higher quotas, head to API keys to mint a permanent key starting with sk_live_.

3
Initialise the client
from aliax import Aliax

# Zero-config: works immediately out of the box with no key!
aliax = Aliax()

# Or authenticate with a personal key:
# aliax = Aliax(api_key="sk_live_...")  # or set os.environ["ALIAX_API_KEY"]
4
The 3-line agent loop
from playwright.async_api import async_playwright

async with async_playwright() as p:
    page = await (await p.chromium.launch()).new_page()
    await page.goto("https://shop.example.com/cart")

    # 1. Translate the live DOM → Set-of-Mark image + JSON node map.
    ctx = await aliax.parse_ui(page, render_config={"format": "jpeg", "quality": 70})
    #    ctx.image_bytes  — JPEG (default) with numbered Set-of-Mark boxes.
    #    ctx.elements     — [{element_id, tag, bounds, text, ...}, ...]

    # 2. Hand BOTH to your VLM. It replies with a structured action.
    decision = await ask_llm(image=ctx.image_bytes, map=ctx.elements)
    #   → {"action": "CLICK", "element_id": "el_41"}

    # 3. Aliax executes natively — scroll-into-view, React onChange,
    #    iframe coord math, retina DPR, all handled.
    result = await aliax.execute(page, decision)
    print(result)  # {'ok': True, 'action': 'CLICK', 'coords': [905, 150], ...}
5
Safety net — capture failures for annotation
# When the agent loops past recovery, dump the state to the queue:
await aliax.capture_failure(
    page,
    goal="Close newsletter popup",
    thoughts=agent.current_reasoning,
    last_attempted_action={"action": "CLICK", "element_id": "el_41"},
    failure_reason="modal_blocked",
    step=agent.loop_step,
)
# Then open /captures to see it land and tap the right element.
Test without a browser
Pass debug_mode=True (Python) / debugMode: true (Node) to the constructor and capture_failure writes aliax_debug_payload.json + aliax_debug_screenshot.jpg to tempfile.gettempdir() instead of POSTing. The returned capture_id is stubbed as debug_capture_<uuid8>, and version-check + telemetry pings are skipped.
execute() never throws
Your agent loop stays alive even if a click fails — invalid input resolves to {ok: False, error: "..."} instead of an exception. Check result["ok"] and keep stepping.
Get started

Architecture

Every call to parse_ui strips a typical page from 100,000 tokens of raw HTML down to around 1,500 tokens — just the interactive elements your model needs to decide what to do next. Aliax is three parts: the SDK (Python or Node) that runs in your agent process, a Cloudflare Worker at the edge, and a Postgres + R2 database for capturing failures and storing screenshots.

The three product pillars

The Compactor
parse_ui()
  • 100k → 1.5k tokens
  • Strips Tailwind + tracking
  • Set-of-Mark image included
  • render_config quality knob
The Steady Hand
execute()
  • data-aliax-id stamped on every node
  • Five-tier execution resolver
  • CLS-immune across reflow
  • React-safe TYPE descriptor bypass
The Airbag
report_issue()
  • Confirms agent is stuck before escalating
  • Screenshot + DOM map + thoughts
  • Visual replay in dashboard
  • JSONL export anytime

How clicks stay accurate after page changes

When parse_ui() runs, the DOM mapper writes a data-aliax-id attribute on every interactable element before the screenshot is taken:

html
<button data-aliax-id="el_41" aria-label="Add to cart">Add to cart</button>

execute() resolves the target via document.querySelector('[data-aliax-id="el_41"]') — never by coordinate. This makes every click CLS-immune: React re-renders, lazy hydration, ad reflow, and viewport resize cannot shift the target out from under the executor. The stamp is rewritten on every parse_ui() call so it always reflects the live DOM.

Deployment topology

aliax (Python + Node)
Interceptor in your agent
  • DOM mapper → node JSON
  • Browser-native overlay (CSS)
  • execute() runs the action
  • <10ms overlay paint
Cloudflare Worker
Edge ingest + telemetry
  • POST /v1/telemetry (usage)
  • POST /v1/capture (failures)
  • SHA-256 key auth
  • R2 + Postgres writes
Aliax Cloud
Storage + UI
  • R2 (aliax-screenshots)
  • Supabase Postgres
  • usage_events ledger
  • Captures dashboard

The DOM scan and screenshot happen entirely inside your browser — nothing leaves the machine on Aliax's critical path. The numbered overlay is drawn by the bundled script and captured by Playwright directly. No extra image processing step. A tiny background ping records usage for billing. Only capture_failure() actually sends a screenshot to the cloud — and if that upload fails, your agent keeps running. Screenshots are served from a CDN; the API server is never in the read path.

Integration guides

1. Standalone Playwright

The pure, zero-dependency local loop. Import Aliax, call parse_ui(), send the compact payload to your VLM, and call execute(). No frameworks, no orchestrator — just you, Playwright, and your model of choice.

Best for
Custom agents you're building from scratch. You own the loop and want a simple, dependency-free perception and execution layer with a ready-made system prompt and identical API across Python and Node.

The complete loop

from playwright.async_api import async_playwright
from aliax import Aliax, SYSTEM_INSTRUCTIONS

# Zero-signup: starts keyless via free local sandbox (~/.aliax/credentials).
# Or pass your personal key: Aliax(api_key=os.environ["ALIAX_API_KEY"])
aliax = Aliax()
goal = "Add the cheapest in-stock item to the cart and check out."

async with async_playwright() as p:
    page = await (await p.chromium.launch()).new_page()
    await page.goto("https://shop.example.com/cart")

    while True:
        # 1. SEE — CLS-immune perception: 1.5k visual map + Set-of-Mark image
        ctx = await aliax.parse_ui(page)

        # 2. THINK — your VLM picks an element_id, not a pixel
        decision = await ask_llm(
            image=ctx.image_bytes,
            elements=ctx.elements,
            system=SYSTEM_INSTRUCTIONS,
        )
        if decision["action"] == "DONE":
            break

        # 3. ACT — CLS-immune execution via data-aliax-id (0 ghost clicks)
        result = await aliax.execute(page, decision)
        if not result["ok"]:
            # Aliax checks whether the page is actually stuck before escalating.
            await aliax.report_issue(
                page,
                reason="execute_failed",
                goal=goal,
                last_attempted_action=decision,
            )
            break
Integration guides

2. Headless cloud (Browserbase / Steel)

Aliax works with any headless cloud browser provider — Browserbase, Steel, Browserless, or your own setup. The DOM scan and screenshot run inside the remote browser. The only thing that travels over the network each step is the compact element list (~1,500 tokens) plus the screenshot — never the full page HTML.

Best for
Teams running agents in production who need session isolation, residential proxies, CAPTCHA solving, or massive parallel concurrency from a managed browser host.

1. Mint a Browserbase session (or Steel)

Both providers expose a REST endpoint that returns a CDP URL. Browserbase: POST https://www.browserbase.com/v1/sessions. Steel: POST https://api.steel.dev/v1/sessions. Same shape, swap the host and the auth header.

import os, httpx

async def mint_browserbase_cdp_url() -> str:
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.post(
            "https://www.browserbase.com/v1/sessions",
            headers={"x-bb-api-key": os.environ["BROWSERBASE_API_KEY"]},
            json={"projectId": os.environ["BROWSERBASE_PROJECT_ID"]},
        )
        r.raise_for_status()
        return r.json()["connectUrl"]

# Steel variant — identical shape, different host:
#   POST https://api.steel.dev/v1/sessions
#   Header: Steel-Api-Key: <key>
#   Response: { "websocketUrl": "wss://..." }

2. Connect Playwright, drive Aliax

from playwright.async_api import async_playwright
from aliax import Aliax

aliax = Aliax(api_key=os.environ["ALIAX_API_KEY"])
cdp_url = await mint_browserbase_cdp_url()

async with async_playwright() as p:
    browser = await p.chromium.connect_over_cdp(cdp_url)
    try:
        page = await browser.new_page()
        await page.goto("https://target.example.com")

        # The Compactor runs INSIDE the remote browser — no extra round-trip.
        ctx = await aliax.parse_ui(page)
        decision = await ask_llm(image=ctx.image_bytes, elements=ctx.elements)
        await aliax.execute(page, decision)
    finally:
        await browser.close()  # release the Browserbase session
Always release the session
Browserbase and Steel meter sessions by the minute. The finally / browser.close() pattern above guarantees the remote sandbox shuts down even if your agent loop raises mid-run.
Integration guides

3. Agent framework (Browser-Use / Stagehand)

Already using Browser-Use, Stagehand, or LangChain? You don't need to rewrite your agent. Add Aliax as a per-step hook — about 10 lines — to get accurate clicks that survive layout shifts and SPAs, plus visual crash captures when something goes wrong.

No plug-and-play integration
Aliax doesn't ship a ready-made plugin for Browser-Use or Stagehand. The integration is a manual hook — about 10 lines, shown below. This is intentional: you keep full control of your agent loop.
Why add Aliax to Browser-Use or Stagehand?
Browser-Use and Stagehand provide great planning and agent loop abstractions. Aliax provides the missing production reliability runtime:
  • Zero ghost clicks: data-aliax-id binds to live DOM elements at execution time — layout shifts, SPA re-renders, and ad reflows never cause misclicks.
  • Collision-free Set-of-Marks: Numbered badges are placed deterministically without overlapping or obscuring critical text on dense dashboards.
  • Full shadow DOM & iframe coverage: Recursively walks into shadow roots and iframes, projecting coordinates back seamlessly.
  • Stagnation-proof telemetry: When your agent loops or stalls at step 12, Aliax proves stagnation and captures screenshots, DOM maps, and thoughts to your dashboard.

Per-step hook (Browser-Use)

Browser-Use exposes lifecycle hooks. Call aliax.parse_ui() before each model decision to pass Aliax's CLS-immune element map and Set-of-Mark image to your agent state, and use aliax.execute() to execute actions deterministically:

import os
from browser_use import Agent, AgentHooks
from aliax import Aliax

aliax = Aliax(api_key=os.environ["ALIAX_API_KEY"])

class AliaxHooks(AgentHooks):
    async def on_step_start(self, agent_state, browser_context):
        page = await browser_context.get_current_page()
        ctx = await aliax.parse_ui(page)
        # Compact node map (~1.5k tokens) + collision-free Set-of-Mark image.
        # Supplies CLS-immune element IDs and shadow DOM resolution.
        agent_state.aliax_ctx = ctx

agent = Agent(
    task="Find the cheapest non-stop flight from JFK → LHR next Friday.",
    llm=my_llm,
    hooks=AliaxHooks(),
)

await agent.run()

Visual telemetry on error (Stagehand / LangChain / custom)

If your framework throws on a failed action, capture_failure() snapshots the live page and sends it straight to your annotation queue — no stall check, fires on every caught error. Use this for the "something broke in production, show me the screenshot" path.

try:
    await stagehand.act(instruction)
except Exception as e:
    # Unconditional capture: snapshots the page, posts to your annotation queue.
    await aliax.capture_failure(
        page,
        goal=current_goal,
        thoughts=agent.last_reasoning,
        last_attempted_action={"action": "CLICK", "element_id": e.target},
        failure_reason="stagehand_error",
    )

Report a stuck loop

If the agent keeps trying the same action and nothing changes, call report_issue(). Aliax checks whether the page has actually stalled before logging the failure — it will only escalate after seeing 3 identical page states or the same action repeated in a cycle. That means it's safe to call on every step; it won't cry wolf. reason is required.

await aliax.report_issue(
    page,
    reason="suspected_stall",
    goal=current_goal,
    thoughts=agent.last_reasoning,
    last_attempted_action=last_decision,
)
Core concepts

Authentication

You can run Aliax right now — no account needed. Or add an API key to unlock the dashboard, live telemetry, and crash captures.

1. Free sandbox — no signup required

Call Aliax() or new Aliax() with no API key and Aliax works immediately. No account, no credit card. On first use, the SDK quietly creates and saves a free credential to your machine so you don't have to do anything.

PropertySpecification
Allowance500 free parses on home/office ISP connections · 100 free parses on cloud/datacenter egress (AWS, GCP, etc.)
Local storage~/.aliax/credentials (%USERPROFILE%\.aliax\credentials on Windows) with 0600 file permissions
LifecycleValid for 30 days, then silently re-bootstraps if quota remains
Allowed operationsFull DOM parsing (parse_ui), Set-of-Mark generation, and action execution (execute)
ExclusionsWeb dashboard and visual crash captures (capture_failure) require a personal key

2. Personal workspace API key (sk_live_...)

Keys are minted from 32 bytes of crypto.getRandomValues and rendered into a 32-character lowercase Crockford-style alphabet (digits 2-9, letters a-z excluding l and o) — shown as sk_live_<32 chars> (40 total characters including the prefix). We store only the SHA-256 hash; the plaintext is shown exactly once.

A personal key grants access to the web dashboard, live telemetry, and visual crash captures (POST /v1/capture). Every free account includes 1,000 starter credits.

3. Enterprise fail-closed opt-out

For SOC2 compliance or air-gapped CI/CD environments that must never make unauthenticated background requests, pass allow_anonymous=False (Python) or allowAnonymous: false (Node), or set the environment variable ALIAX_DISABLE_ANONYMOUS=1. When opted out, missing keys throw immediately instead of minting a sandbox token.

How the SDK sends credentials

The client adds the key as an Authorization: Bearer <key> header on every request (telemetry and capture). The plaintext token never appears in the request body, never lands in multipart parts, and is never logged at proxies, CDNs, or in Worker request logs. The Worker reads it exclusively from this header — a request that puts the key in a form field will be rejected with 401 missing_api_key.

Rotation & revocation

Revoke from API keys. A revoked key returns 401 revoked_api_key on the next call. Rotate quarterly and on any suspected leak.

Never embed keys in client-side JS
Personal keys grant write access to your capture inbox and billing ledger. They belong in the agent process only — load from os.environ or a secret manager.
Core concepts

Pre-packaged system instructions

Don't write your own system prompt. Aliax ships a ready-made, versioned instruction string — SYSTEM_INSTRUCTIONS — that tells your model exactly how to read the page and what actions are valid.

Never write prompts manually
The most common cause of visual agent failures is a bad system prompt: models click phantom coordinates, invent actions that don't exist, or spin forever on a disabled button. Importing SYSTEM_INSTRUCTIONS fixes all of this out of the box.

How to import

Both the Python and Node SDKs export SYSTEM_INSTRUCTIONS and its alias ALIAX_SYSTEM_INSTRUCTIONS directly from the package root:

from aliax import Aliax, SYSTEM_INSTRUCTIONS

# Pass SYSTEM_INSTRUCTIONS directly to your model's system prompt

What the prompt handles automatically

CapabilityHow it protects your agent
Set-of-Mark ProtocolInstructs the model how to read numbered bounding boxes (el_41) and correlate them with the element text block rather than hallucinating raw pixel coordinates.
State FlagsEnforces strict rules around DISABLED, REQUIRED, INVALID, BUSY, READONLY, preventing dead loops on unclickable controls.
Navigation PlanningDirects the model to use -> /route hints to choose the shortest path, and verifies active page route changes after navigation clicks.
Action VocabularyRestricts model output strictly to verbs accepted by execute(): CLICK, TYPE, TYPE_AND_ENTER, HOVER, SCROLL, REPORT_ISSUE, DONE.
Structured EscalationDictates the Expectation-vs-Reality JSON contract for REPORT_ISSUE when an agent is genuinely stuck, enabling automated crash captures.
Invisible UpgradesAs new actions and DOM heuristics ship with SDK updates, upgrading the package updates your agent's reasoning automatically without changing your code.

Plugging into popular LLM providers

# OpenAI (GPT-4o)
from openai import AsyncOpenAI
from aliax import SYSTEM_INSTRUCTIONS

openai = AsyncOpenAI()
res = await openai.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": SYSTEM_INSTRUCTIONS},
        {
            "role": "user",
            "content": [
                {"type": "text", "text": f"Goal: {user_goal}\n\nDOM Map:\n{ctx.llm_text_block()}"},
                {"type": "image_url", "image_url": {"url": f"data:{ctx.image_mime};base64,{ctx.image_base64}"}},
            ],
        },
    ],
    response_format={"type": "json_object"},
)
decision = json.loads(res.choices[0].message.content)
await aliax.execute(page, decision)
Core concepts

Capture API

A single endpoint. Multipart in, JSON out.

POSThttps://api.aliax.xyz/v1/capture

cURL

bash
curl -X POST https://api.aliax.xyz/v1/capture \
  -H "Authorization: Bearer sk_live_..." \
  -H "X-Aliax-SDK-Version: 1.0.5" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -F "goal=Add the Medium size to cart" \
  -F "ai_thoughts=Targeting the green dismiss button at top-right of modal." \
  -F 'last_attempted_action={"action":"CLICK","target_x":905,"target_y":150,"selector":".modal-close","element_id":"el_88"}' \
  -F "failure_reason=element_not_found" \
  -F "step=3" \
  -F 'viewport={"width":1920,"height":1080,"dpr":2}' \
  -F 'spatial_map=[{"element_id":"el_1","role":"button","label":"Close","bbox":[1820,40,80,40]}]' \
  -F "sdk_version=1.0.5" \
  -F "screenshot=@./snag.jpg;type=image/jpeg"

Limits

LimitValue
Max screenshot10 MB
Accepted MIMEimage/jpeg · image/png · image/webp (SDK sends JPEG by default; WebP still accepted for custom uploaders)
Request timeout (server)30 s
SDK HTTP client (default)10 s per non-capture call
SDK capture timeout15 s per attempt · 3 attempts with linear backoff (retries on 429/502/503/504)
Core concepts

Request payload

Every field is a form field. JSON objects (like the spatial map) are passed as a JSON string in that field — the SDK handles this for you.

FieldTypeRequiredNotes
goalstringyesPlain-English task. Becomes the annotation prompt.
screenshotfileyesJPEG (default), PNG, or WebP. ≤ 10 MB.
ai_thoughtsstringrecommendedAgent chain-of-thought. ≤ 32 KB. Renders in the annotator side panel.
last_attempted_actionJSON objectrecommendedThe stupid move the agent made — drives the red-X overlay.
failure_reasonstringrecommendedShort tag: element_not_found, timeout, etc.
stepintegeroptionalAgent loop iteration counter.
spatial_mapJSON arrayrecommendedNumbered node list from parse_ui / parseUi. Powers point-in-bbox lookups.
viewportJSON objectrecommended{width, height, dpr}. DPR scales the overlay on retina screenshots.
contextstringoptionalLegacy. Free-form blob; v0.2 promotes JSON fields server-side.
sdk_versionstringautoStamped by the SDK. Surfaces in error reports.
Core concepts

Agent telemetry

Send the agent's reasoning and the action it tried, and Aliax turns every failure into a complete training example — the wrong move and the human-corrected right move, side by side.

Why it matters

A screenshot shows you what the page looked like. The attempted action shows you what the model tried to do. Together, the dashboard can pin a pulsing red-X exactly where the agent clicked, look up which element was actually at those coordinates, and tell you at a glance: "agent clicked a wrapper div when it should have clicked <button>Submit</button>."

The attempted-action shape

from dataclasses import dataclass
from typing import Optional

@dataclass
class AttemptedAction:
    action: str                          # "CLICK" | "TYPE" | "SCROLL" | "HOVER" | "NAVIGATE" | ...
    target_x: Optional[int] = None       # CSS pixels, viewport-relative
    target_y: Optional[int] = None
    value: Optional[str] = None          # text typed (TYPE actions)
    selector: Optional[str] = None       # css/xpath the agent THOUGHT it was hitting
    element_id: Optional[str] = None     # the el_<N> id from parse_ui, if any

    def to_dict(self) -> dict: ...       # asdict() with None values stripped

Either pass an AttemptedAction instance or a plain dict/object — the SDK accepts both. Coordinates are CSS pixels in the same coordinate space as spatial_map[].bbox, so the dashboard can do bounding-box lookups directly.

End-to-end example

import os
from aliax import Aliax, AttemptedAction

aliax = Aliax(api_key=os.environ["ALIAX_API_KEY"])

await aliax.capture_failure(
    page=page,
    goal="Close the newsletter modal and continue to checkout.",
    thoughts=agent.current_reasoning,
    last_attempted_action=AttemptedAction(
        action="CLICK",
        target_x=905,
        target_y=150,
        selector=".modal-close",
    ),
    failure_reason="element_not_found",
    step=agent.loop_step,
)

How the dashboard uses it

  1. Reads last_attempted_action.target_x / target_y and pins a pulsing red-X marker on the screenshot at the scaled pixel.
  2. Runs a point-in-bbox lookup against the spatial map to find which element the agent actually hit (often a wrapper, a backdrop, or empty space).
  3. Renders ai_thoughts in the side panel so the annotator sees the model's reasoning, not just the outcome.
  4. On annotation, writes back corrected_action alongside the original attempt — a complete (prompt, rejected, chosen) tuple for DPO.

Retina / HiDPI

The SDK reads window.devicePixelRatio inside the same __AliaxCore.mapDOM() call that returns the spatial map, then ships it as viewport.dpr. The dashboard scales the overlay accordingly, so a screenshot taken at 2× on a Retina display still lands the red-X on the right pixel.

Backwards compatible
Existing v0.1 callers passing only context= keep working untouched. If you packed a JSON blob with thoughts / last_attempted_action into context, the Worker auto-promotes those fields into the new columns — no migration needed.
Core concepts

Response shape

201 — success

json
{
  "status": "success",
  "capture_id": "8f3c2a64-9c2e-4a3b-9d20-1f4e1c6e2b51",
  "screenshot_url": "https://cdn.aliax.xyz/<user_id>/8f3c2a64-….jpg",
  "msg": "Captured."
}

4xx / 5xx — error envelope

json
{
  "status": "error",
  "code": "invalid_api_key",
  "msg": "Unknown API key"
}

The SDK normalises both shapes into {status, capture_id, msg} so call sites don't need to branch on HTTP status.

Core concepts

Spatial map

The spatial map is the numbered list of every clickable element on the page — buttons, inputs, links — each with its position, label, and role. It's what lets the annotator tap a spot on the screenshot and know exactly which element the agent should have hit.

Envelope shape (v0.2)

json
{
  "elements": [ /* node[] */ ],
  "viewport": { "dpr": 2, "width": 1920, "height": 1080 }
}

The SDK extracts elements for the spatial_map field and copies viewport.dpr into the top-level viewport object. v0.1 clients that read a bare array still work — the SDK's extractor falls back to result.elements ?? result.

Node shape

json
{
  "element_id": "el_14",
  "role": "button",
  "label": "Close",
  "bbox": [1820, 40, 80, 40],
  "z": 999,
  "in_viewport": true,
  "frame": "main"
}
FieldDescription
element_idStable string id for this capture (e.g. "el_14"). Used verbatim as target_element_id in the annotated output and as the element_id argument to execute().
roleInferred role — button, link, input, checkbox, …
labelVisible text or aria-label, redacted of any PII matches.
bbox[x, y, w, h] in CSS pixels, relative to the viewport top-left.
zEffective stacking order. The annotator UI uses this to disambiguate overlapping taps.
in_viewportFalse for elements present but scrolled off-screen at capture time.
frameIframe path, e.g. main or main > checkout-iframe.
The mapper traverses shadow DOM and same-origin iframes
We walk past shadowRoot and into reachable iframes, then project every child bbox back into the parent page's viewport coordinate system. You don't need to do anything special for component libraries that use shadow DOM (Stencil, Lit, FAST).
Core concepts

Error codes

Errors are never silent. Every non-2xx response carries a stable code string — log against this, not against the HTTP status.

HTTPCodeMeaning
400invalid_bodyRequest was not valid multipart/form-data.
400missing_goalgoal field is required.
400missing_screenshotscreenshot file part is required.
400invalid_spatial_mapspatial_map must be a JSON array.
400ai_thoughts_too_longv0.2 — ai_thoughts exceeded the 32 KB cap. Truncate client-side.
400invalid_last_attempted_actionv0.2 — target_x / target_y must be numeric when provided.
401missing_api_keyAuthorization header absent or not prefixed with `Bearer sk_`.
401invalid_api_keyKey hash did not resolve to an account.
401revoked_api_keyKey was revoked from the dashboard.
401expired_api_keyKey passed its expires_at.
413payload_too_largeScreenshot exceeded the 10 MB cap.
502storage_failedR2 rejected the screenshot. Retry.
502db_insert_failedMetadata insert failed. The screenshot is GC'd. Retry.
500internal_errorUnhandled. Logged server-side, report with the capture id.
SDK reference

SDK reference

One API surface, two languages. Every method is identical in behavior — Python uses snake_case, Node/TypeScript uses camelCase. The wire format and the action verbs inside decision objects stay snake_case in both because they are JSON keys, not language identifiers.

aliax 1.0.5 · Python 3.8 – 3.12 · async / httpx

Install

pip install aliax
playwright install chromium

aliax ships pure-Python with one runtime dep (httpx). The bundled DOM-mapper JS is zip-safe inside the wheel — no Pillow, no native extensions, no CDN fetch at runtime.

Constructor

from dataclasses import dataclass
from typing import Optional, Union, Mapping, Any, List

@dataclass
class AttemptedAction:
    action: str
    target_x: Optional[int] = None
    target_y: Optional[int] = None
    value: Optional[str] = None
    selector: Optional[str] = None
    element_id: Optional[str] = None

@dataclass
class ParseContext:
    image_bytes: bytes          # JPEG (default) screenshot with numbered SoM boxes
    image_mime: str             # "image/jpeg" or "image/png"
    image_size: tuple           # (width, height)
    elements: List[dict]        # [{element_id, tag, bounds, text, ...}]
    viewport: dict              # {width, height, dpr, scroll_x, scroll_y}
    url: str
    path: str
    title: str
    truncated: bool

class Aliax:
    def __init__(
        self,
        api_key: Optional[str] = None,         # falls back to os.environ["ALIAX_API_KEY"], else free anonymous sandbox
        allow_anonymous: bool = True,          # True = auto-bootstrap sk_anon_...; False = fail-closed
        endpoint: str = "https://api.aliax.xyz/v1",
        debug_mode: bool = False,
        redact_selectors: Optional[list] = None,
        check_for_updates: bool = True,
        max_image_dim: int = 0,                # DEPRECATED — no-op since v1.0
        fallback_endpoint: Optional[str] = None,  # auto-set on the public default
    ): ...

    # Async context manager — deterministic httpx pool teardown.
    async def __aenter__(self) -> "Aliax": ...
    async def __aexit__(self, *exc) -> None: ...
    async def aclose(self) -> None: ...

Constructor arguments

ArgumentDefaultPurpose
api_keyNone / $ALIAX_API_KEYOptional. Your sk_live_… token or env var. When omitted, the SDK automatically bootstraps a free machine-scoped sandbox credential (~/.aliax/credentials).
allow_anonymousTrue / trueWhen True, automatically provisions a free sandbox credential if no API key is set (500 parses residential / 100 cloud). Pass False (or set ALIAX_DISABLE_ANONYMOUS=1) to fail closed.
endpointapi.aliax.xyz/v1Override during private-cloud or self-hosted runs.
fallback_endpointNone (auto on default endpoint)When endpoint is the public default, the SDK wires in a workers.dev fallback automatically.
debug_modeFalseWrites aliax_debug_payload.json + aliax_debug_screenshot.jpg to tempfile.gettempdir() instead of POSTing; skips telemetry pings and the version check.
redact_selectorsNoneCSS selectors to visually blur on every screenshot and strip from spatial-map labels.
check_for_updatesTruePings /v1/version once with a 1s timeout. Suppressed in debug mode.

parse_ui()

async def parse_ui(
    self,
    page,
    *,
    render_config: Optional[Mapping[str, Any]] = None,
    #     {"format": "jpeg" | "png",   # default "jpeg"
    #      "quality": 30..100}          # default 80, clamped to floor 30
    draw_overlay: bool = True,
    min_size: int = 12,
    max_elements: int = 200,
    # Legacy kwargs — folded into render_config when not provided.
    max_image_dim: Optional[int] = None,   # DEPRECATED no-op (logs once)
    image_format: Optional[str] = None,    # legacy alias for render_config.format
    image_quality: Optional[int] = None,   # legacy alias for render_config.quality
) -> ParseContext: ...                     # raises AliaxInvalidKeyError / AliaxOutOfCreditsError

What happens on every call

  1. Aliax's DOM mapper is injected once per page — safe to call on every step, won't double-inject.
  2. Two concurrent parse_ui calls on the same page are serialised so they don't stomp each other's overlay.
  3. A navigation stamp is recorded so Aliax can detect when the page changed and came back to the same URL — preventing stale reads.
  4. Any elements matching your redact_selectors (plus passwords, emails, card numbers by default) are blurred before the screenshot is taken.
  5. The DOM is walked and every visible, interactive element is collected with its position and label — in one browser round-trip.
  6. Numbered colored boxes are painted over the screenshot — Chromium renders 200 boxes in under 2 ms.
  7. The screenshot is encoded as JPEG or PNG natively. No extra processing step.
  8. The overlay is removed and PII fields are unblurred before the call returns — the page is left exactly as found.
  9. The element list is cached so execute() can act on an element_id without re-parsing.
  10. Usage is reported in the background when your balance is healthy. When balance hits zero, Aliax waits for the balance confirmation before the next parse, so a top-up takes effect immediately.

execute() — supported actions

# --- Pointer / keyboard ---
await aliax.execute(page, {"action": "CLICK",          "element_id": "el_41"})
await aliax.execute(page, {"action": "HOVER",          "element_id": "el_14"})
await aliax.execute(page, {"action": "TYPE",           "element_id": "el_7",  "value": "Nike Shoes"})
await aliax.execute(page, {"action": "TYPE_AND_ENTER", "element_id": "el_7",  "value": "Nike Shoes"})
await aliax.execute(page, {"action": "PRESS",          "element_id": "el_7",  "key": "Enter"})

# --- Scrolling (directional shortcuts scroll ~80% of viewport / container) ---
await aliax.execute(page, {"action": "SCROLL_DOWN"})
await aliax.execute(page, {"action": "SCROLL_UP",   "element_id": "el_22"})  # scroll inside a container
await aliax.execute(page, {"action": "SCROLL_LEFT"})
await aliax.execute(page, {"action": "SCROLL_RIGHT"})
await aliax.execute(page, {"action": "SCROLL",      "dy": 600})              # explicit delta

# --- Navigation / pacing / completion ---
await aliax.execute(page, {"action": "NAVIGATE", "url": "https://example.com"})  # http(s) only — SSRF guard
await aliax.execute(page, {"action": "WAIT",     "ms": 1500})
await aliax.execute(page, {"action": "DONE"})    # also: "FINISH", "NOOP"

# --- Composite verbs ---
await aliax.execute(page, {
    "action": "BATCH_TYPE",
    "inputs": [
        {"element_id": "el_3", "value": "jane@example.com"},
        {"element_id": "el_4", "value": "hunter2"},
    ],
})
await aliax.execute(page, {
    "action": "COMBO",                    # exactly 2 sub-actions, no nesting
    "actions": [
        {"action": "CLICK", "element_id": "el_8"},
        {"action": "TYPE",  "element_id": "el_9", "value": "Hello"},
    ],
})

# --- Escalation (Gatekeeper-gated; rejected unless stagnation is proven) ---
await aliax.execute(page, {
    "action": "REPORT_ISSUE",
    "reason": "submit_unresponsive",
    "context": {
        "expected_outcome": "Navigate to /confirmation",
        "actual_outcome":   "Still on /checkout after 3 identical parses",
    },
})

# --- Raw-coords escape hatch (when you have your own mapper) ---
await aliax.execute(page, {"action": "CLICK", "x": 905, "y": 150})

execute() handles the messy bits natively: scroll-into-view before clicking, click-then-focus-then-type with a 50ms keystroke delay so React onChange listeners fire, iframe coordinate offsets, and retina DPR. It never throws — invalid input returns {ok: False, error: "..."}. The result's execution_tier is one of "locator", "jsclick", "rebind", "coords", or "blocked_disabled".

capture_failure() & report_issue()

# Direct capture — always posts to /v1/capture (or writes to tmp in debug_mode)
cap = await aliax.capture_failure(
    page,
    goal="Complete checkout",
    thoughts="The submit button is visible but clicking does nothing.",
    last_attempted_action={"action": "CLICK", "element_id": "el_42"},
    failure_reason="submit_unresponsive",
    step=7,
    context={"expected_outcome": "/confirmation", "actual_outcome": "still on /checkout"},
)
# cap["status"]: "success" | "error"
# cap["capture_id"]: str | None
# cap["screenshot_url"]: str (when success)

# Gatekeeper-gated escalation — rejected unless either
#   (a) three consecutive parse_ui rounds returned the same state hash, OR
#   (b) a period-2/3/4 action cycle was observed across distinct element_ids.
issue = await aliax.report_issue(
    page,
    reason="submit_button_dead_zone",
    expected_outcome="Page navigates to /confirmation after clicking Submit.",
    actual_outcome="Page stays on /checkout. 3 identical parse states observed.",
    goal="complete checkout",
    step=7,
    # force=True,   # bypass the Gatekeeper (developer assertion)
)
if issue["status"] == "rejected":
    print(issue["msg"])     # explains exactly what proof is still missing

capture_failure hard-codes the screenshot at type:"jpeg", quality:85 and uses 3 attempts with linear backoff (1500ms × (attempt+1)) on 429/502/503/504. In debug mode it writes the payload + screenshot to the temp dir and returns a debug_capture_<uuid8> id with no network call.

capture_failure() / captureFailure() arguments

ArgumentSincePurpose
page0.1Playwright Page handle.
goal0.1Plain-English task the agent was attempting.
thoughts0.2Agent chain-of-thought right before the snag.
last_attempted_action0.2The "stupid move" — AttemptedAction or dict/object. Drives the red-X overlay.
failure_reason0.2Machine-friendly tag: element_not_found, timeout, stale_dom.
step0.2Agent loop iteration counter.
context0.1Legacy. Free-form string OR dict; JSON dicts containing v0.2 fields are auto-promoted server-side.

Billing & lifecycle

# Pure in-memory snapshot — no network call.
status = aliax.billing_status()
# {"locked": bool, "reason": str | None, "balance": int | None, "credits_exhausted": bool}

# Force a fresh server-side balance check (one 0-cost telemetry ping):
await aliax.refresh_billing_status()

# Deterministic teardown — closes the httpx pool.
await aliax.aclose()

# Or use as an async context manager:
async with Aliax(api_key=os.environ["ALIAX_API_KEY"]) as aliax:
    ctx = await aliax.parse_ui(page)
    # ... aclose() runs at scope exit, even on exception

Error classes

from aliax import (
    AliaxError,             # base — catch this for any SDK error
    AliaxInvalidKeyError,   # 401 — terminal; SDK locks permanently
    AliaxOutOfCreditsError, # 402 after grace loop; self-heals on top-up
)

try:
    await aliax.parse_ui(page)
except AliaxOutOfCreditsError as e:
    print("balance:", e.balance)        # last known balance (negative or None)
except AliaxInvalidKeyError as e:
    print("reason:", e.reason)          # mint a new key
except AliaxError:
    # any other SDK-originated failure
    raise
Python ↔ Node casing cheatsheet
Public names follow each language's convention: parse_ui ↔ parseUi, capture_failure ↔ captureFailure, last_attempted_action ↔ lastAttemptedAction, api_key ↔ apiKey. The action verbs inside decision objects and the spatial-map field names (element_id, bounds) stay snake_case in both SDKs because they are JSON keys.
SDK reference

PII redaction

Aliax does not auto-detect what counts as sensitive on your app. You tell it which elements to blur — it blurs them before the screenshot and strips their text from the element list. Nothing else leaves the page.

Layer 1 — visual blur

Pass CSS selectors to redact_selectors. Anything matching is given a heavy CSS filter: blur(12px) for the duration of the screenshot, then unblurred.

aliax = Aliax(
    api_key=os.environ["ALIAX_API_KEY"],
    redact_selectors=[
        "[data-pii]",
        ".customer-email",
        "input[name='credit_card']",
    ],
)

Layer 2 — text strip on the spatial map

The DOM mapper runs the same selector list against label values before they leave the page, replacing them with [REDACTED]. Bboxes and roles are preserved so annotators still see the layout.

You own the selector list
We don't run heuristic PII detection — false positives in an enterprise annotation pipeline are unacceptable. Treat the selector list as part of your DPIA and review it whenever your customer's UI changes.
SDK reference

Versioning

The DOM mapper is bundled inside the SDK package — it never fetches anything from a CDN at runtime. This is deliberate: a silent update mid-run could change what the element list looks like and corrupt your captures. What you install is what runs.

Active-degradation notice

On construction (when check_for_updates=True), the SDK pings GET /v1/version with a 1 s timeout. If you're behind, a single warning fires — no exceptions, no retry storm.

Pinning

pip install aliax

Pin in production. The mapper output schema is part of the SDK version contract, so upgrading mid-training would invalidate cached spatial maps.

Platform

Annotation flow

The moment a human annotates a capture, it becomes a complete training example: the wrong action the agent took, and the correct action the human provided — side by side, ready to export.

  1. The capture lands in Captures with status: unannotated.
  2. The inspector renders the screenshot with a pulsing red-X at the agent's last_attempted_action.target_x/y (scaled by viewport.dpr), the goal, and ai_thoughts in the side panel.
  3. The platform cross-references the red-X against the spatial map and surfaces which element the agent actually hit — usually a wrapper, backdrop, or empty space.
  4. The annotator taps the element the agent should have hit. We snap by point-in-bbox + z-index and prompt for the action type (CLICK, TYPE, HOVER, SCROLL).
  5. A corrected_action JSON blob is written back alongside the original last_attempted_action, and status flips to annotated.

Output shape (DPO-ready)

json
{
  "task_id": "8f3c2a64-9c2e-4a3b-9d20-1f4e1c6e2b51",
  "original_goal": "Add the Medium size to cart",
  "ai_thoughts": "Targeting the green dismiss button at top-right of modal.",
  "last_attempted_action": {
    "action": "CLICK",
    "target_x": 905,
    "target_y": 150,
    "selector": ".modal-close",
    "element_id": "el_88",
    "failure_reason": "element_not_found",
    "step": 3
  },
  "ai_attempt_hit_element_id": "el_88",
  "corrected_action": {
    "action": "CLICK",
    "target_element_id": "el_14",
    "target_role": "button",
    "target_label": "Close",
    "value": null,
    "annotated_by": "auth0|6710f1c2…",
    "annotated_at": "2026-06-22T14:03:11.482Z"
  },
  "viewport": { "width": 1920, "height": 1080, "dpr": 2 }
}

The wrong action (last_attempted_action) and the correct action (corrected_action) are in the same row. Export this directly into your model fine-tuning pipeline.

Platform

Billing & credits

Aliax is pay-as-you-go. No plans, no seats, no expiration. One parse_ui() call = one credit.

ItemValue
Free anonymous sandbox500 parses (home/office) · 100 parses (cloud egress) with zero signup
Account signup grant1,000 credits (one-time, on account creation)
Unit price$0.001 / parse (1 USD = 1,000 credits)
Minimum top-up$20 (20,000 credits)
Maximum top-up$10,000 per transaction
ExpirationNone. Credits never expire.
Billed verbsOnly parse_ui. execute, capture_failure, and report_issue are free.
Low-balance warningDashboard surfaces a banner under 500 credits.
PaymentPaystack (cards, bank transfer) and Flutterwave. USD-denominated; FX handled by the provider.

How the SDK sees your balance

The first parse_ui() after process start carries a telemetry ping; the response includes your current balance and stamps the in-memory BillingStatus the SDK exposes via aliax.billing_status() / aliax.billingStatus().

  • While balance > 0 telemetry is fire-and-forget — sub-millisecond overhead per parse.
  • When balance ≤ 0 the SDK awaits the next ping so a top-up flips the kill-switch off within one parse, and a still-empty wallet rejects deterministically with AliaxOutOfCreditsError instead of leaking work.
  • refresh_billing_status() / refreshBillingStatus() sends a zero-cost ping if you want to re-check without parsing.
Self-throttle from the agent loop
Read aliax.billing_status().balance at the top of every loop iteration; pause or alert when it crosses your own threshold (e.g. < 2,000). The SDK's hard kill-switch is a backstop, not a budget tool.
Platform

Best practices

  • Write a specific goal. "Add the Medium size to cart" beats "shop". The annotator sees this verbatim — a vague goal becomes a vague training row.
  • Capture at the snag, not after. The popup or layout shift that broke your agent must still be on screen. Don't page.reload() first.
  • Always send thoughts + last_attempted_action. A capture without them is half a training row. The chain-of-thought is the prompt; the attempted action is the rejected response. Skipping either drops the capture out of your DPO pipeline.
  • Coordinates are CSS pixels, viewport-relative. Use the same coordinate space as page.mouse.click(x, y). DPR is captured automatically — don't pre-multiply by it.
  • Don't dedupe client-side. Two captures of the same popup from different sessions are signal, not noise — a model that fails twice fails consistently.
  • Cap your QPS per agent. aliax.capture_failure() uploads a screenshot — if you're firing >5/s per agent process, you're probably looping on a recoverable error.
Platform

FAQ

Still stuck?

Email support@aliax.xyz with your capture id and we'll trace it end-to-end. Enterprise customers can request a private Slack channel for direct engineering support.