ZeroDOM
Your agent doesn't need the DOM. It needs to know what it can click.
ZeroDOM turns a bloated HTML page into a token-optimized interaction graph — the buttons, inputs and links an agent can actually act on, and nothing else.
An agent driving a browser gets one of two action spaces today, and both are bad.
Pixels are slow, expensive, and produce coordinates that go stale the moment
the page scrolls. The accessibility tree is cheaper but enormous, and it has
no stable handles: 102 of Hacker News' 220 actionable nodes share a
(role, name) pair with another node, so there is no way to say which story to
upvote.
ZeroDOM is a third option — a flat list of what the page can do, where every entry has a stable id, and the addressing information that makes it clickable never enters the context window. About 10 tokens per action, on every page tested.
- No LLM in the loop. lxml in, graph out, 10–30ms, identical output every run.
- Selectors never enter the context window. The model sees
[03]; the CSS path stays inselector_map()on your side. - Nothing leaves your machine. No telemetry, no API keys, no storage — the only network traffic is the page you pointed it at.
- The selectors actually resolve. Verified in a real browser: 231/231 on
Hacker News, where rows carry no
idorclassand numeric ids need escaping.
Install
pip install zerodom
# or: uvx zerodom — the CLI runs straight off PyPI
One extra step only if you use the browser-backed features (from_page,
--render, --screenshot, --html):
playwright install chromium
Quickstart
from zerodom import ZeroDOM
graph = ZeroDOM.from_page(page) # any Playwright page, sync or async
print(graph.to_compact_text()) # what you send the model
selectors = graph.selector_map() # {"node_01": "#email-input", ...} — stays your side
Real output:
PAGE: Orbit — Fleet Console | file:///…/demo/demo-page.html
[01] a 'Fleet'
[05] input 'Search'
[07] input* 'Assigned driver' ph='Search by name'
[15] button 'Dispatch'
[17] button! 'Recall (in transit)'
* marks required, ! marks disabled. The model answers click 15. You resolve
node_15 against selector_map() and click it.
Cost per action
The fair comparison isn't raw HTML — nobody sends a model raw HTML. It's
Playwright's page.aria_snapshot(mode="ai"), which is what Playwright MCP puts
in a model's context.
| page | ZeroDOM | ARIA |
|---|---|---|
| airbnb.com | 9.9 | 23.3 |
| github.com/…/issues | 12.1 | 70.2 |
| en.wikipedia.org article | 11.6 | 41.0 |
| news.ycombinator.com | 10.2 | 47.0 |
| developer.mozilla.org | 9.9 | 46.8 |
tokens per actionable node — measured 2026-08-03
~10 tokens per action, flat across every page, against 23–70 and wildly variable. Mean 71.0% fewer tokens than the snapshot a model actually gets, and 93.1% against raw HTML.
MCP server
{
"mcpServers": {
"zerodom": {
"command": "uvx",
"args": ["--from", "zerodom", "zerodom-mcp"]
}
}
}
Goes in claude_desktop_config.json for Claude Desktop, or .cursor/mcp.json
for Cursor.
| tool | what it does |
|---|---|
zerodom_parse_url(url, verbose=False) |
navigate, return the compact graph |
zerodom_read_page(verbose=False) |
re-read the live DOM without navigating |
zerodom_find(query) |
return only the nodes matching a phrase |
zerodom_click_node(node_id) |
click, then return what changed |
zerodom_fill_node(node_id, text) |
type, then return what changed |
An agent loop shouldn't re-read the page it already has. zerodom_find answers
"where's the dispatch button?" in one line, and actions return a diff — +
appeared, - gone, ~ value changed — instead of re-listing every node.
CLI
zerodom https://news.ycombinator.com # compact graph + token savings
zerodom https://example.com --find checkout # only the nodes that match
zerodom https://example.com --render # headless Chromium, for JS-heavy pages
zerodom https://example.com --json # full JSON graph, selectors included
zerodom https://example.com --html out.html # graph beside an annotated screenshot
Limitations
- Closed shadow roots are unreachable — no browser API exposes them. Open roots work.
- Cross-origin and same-origin iframes are not traversed yet; only the top frame is parsed.
- Canvas-rendered UIs have no DOM to read. Use a vision model there.
- Labels come from the page, so a hostile page can write anything into one. Treat graph text as untrusted input — see SECURITY.md.
- ZeroDOM operates on a
Pageyou already control. It does not bypass bot detection and makes no attempt to.
Links
- Source, full README and benchmarks: https://github.com/DevHusnainAi/zerodom
- Issues: https://github.com/DevHusnainAi/zerodom/issues
- License: Business Source License 1.1 — free to use, including in production; you may not offer ZeroDOM itself as a hosted service. Converts to Apache 2.0 on 2030-08-09.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zerodom-0.0.1.tar.gz.
File metadata
- Download URL: zerodom-0.0.1.tar.gz
- Upload date:
- Size: 159.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a84de1ddd90ccb692e6dacff57376b93771cbb212b43bef5a85ad492e6474586
|
|
| MD5 |
f59afe5eb888f3107d6727d17da70713
|
|
| BLAKE2b-256 |
02aa4870e27bc374c2aea8fae42c95289ec929ec12d1dbdd9ff3af3badcb3bdb
|
Provenance
The following attestation bundles were made for zerodom-0.0.1.tar.gz:
Publisher:
release.yml on DevHusnainAi/zerodom
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zerodom-0.0.1.tar.gz -
Subject digest:
a84de1ddd90ccb692e6dacff57376b93771cbb212b43bef5a85ad492e6474586 - Sigstore transparency entry: 2391524407
- Sigstore integration time:
-
Permalink:
DevHusnainAi/zerodom@03a00bf0b43c95b5507ac966f77bf4dd44d122f1 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/DevHusnainAi
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@03a00bf0b43c95b5507ac966f77bf4dd44d122f1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file zerodom-0.0.1-py3-none-any.whl.
File metadata
- Download URL: zerodom-0.0.1-py3-none-any.whl
- Upload date:
- Size: 27.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
39647af8622fa932bf8bc4c15c3209b002435d1dbd5559a395fe25d17cb8e9ac
|
|
| MD5 |
92adb0e5ba204ad8e38e461c85911787
|
|
| BLAKE2b-256 |
8f8392025b2b473397636d848986a96e4923b82e3c01e7be6b5ffcde8f7bf94a
|
Provenance
The following attestation bundles were made for zerodom-0.0.1-py3-none-any.whl:
Publisher:
release.yml on DevHusnainAi/zerodom
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zerodom-0.0.1-py3-none-any.whl -
Subject digest:
39647af8622fa932bf8bc4c15c3209b002435d1dbd5559a395fe25d17cb8e9ac - Sigstore transparency entry: 2391524465
- Sigstore integration time:
-
Permalink:
DevHusnainAi/zerodom@03a00bf0b43c95b5507ac966f77bf4dd44d122f1 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/DevHusnainAi
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@03a00bf0b43c95b5507ac966f77bf4dd44d122f1 -
Trigger Event:
push
-
Statement type: