Your agent doesn't need the DOM.It needs to know what it can click.
ZeroDOM is an MCP server that turns a web page into a token-optimized interaction graph — Graphify for the browser. Your agent gets a flat list of the buttons, inputs and links it can actually act on, at 71% fewer tokens than the ARIA snapshot your agent gets today, and a predictable ~10 tokens per actionable node across Airbnb, GitHub and Hacker News alike.
<tr class="athing" id="49151933">
<td align="right" valign="top" class="title">
<span class="rank">1.</span></td>
<td valign="top" class="title"><span class="titleline">
<a href="https://example.com/story">Show HN: ZeroDOM
— agents only need to know what they can click</a>
</span></td>
</tr>PAGE: Hacker News
[01] a 'Show HN: ZeroDOM — agents only need to know…'
[02] a 'dev'
[03] a '214 comments'An agent driving a browser gets two action spaces today. 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. ZeroDOM is a third option.
Pixels
vision model + screenshot
- Slow and expensive per step
- Coordinates go stale on scroll
- No stable handle for a repeat element
Accessibility tree
aria_snapshot, Playwright MCP
- Cheaper than pixels, still enormous
- Carries headings, prose and containers to keep its shape
- 102 of Hacker News' 220 nodes share a (role, name) pair
ZeroDOM
flat interaction graph
- Only what the page can do, one line each
- Every id unique by construction
- Selectors resolved on your side, never in context
No LLM in the loop
lxml in, graph out, 10–30ms, identical output every run. Nothing about your page reaches a model until you send the graph to one.
Selectors never enter context
The model sees [03]; the CSS path #row > span > a stays in selector_map() on your side. On real pages those paths cost more tokens than the labels do.
Nothing leaves your machine
The browser is yours, the parse is local, the graph is a dict you own. No telemetry, no API keys, no accounts, no storage.
Measured on real pages, on 2026-08-03. Rerun them yourself.
Both suites ship in the repo — benchmarks/benchmark_tokens.py and benchmarks/benchmark_vs_a11y.py. Rerun them and the counts will have drifted by a few percent.
The fair comparison isn't raw HTML — nobody sends a model raw HTML. It's Playwright's page.aria_snapshot(), specifically mode="ai", which is what Playwright MCP puts in a model's context.
| Page | ARIA | ARIA mode="ai" | ZeroDOM | Targetable by (role, name) | Saved vs ai |
|---|---|---|---|---|---|
| airbnb.comZeroDOM found 170 nodes · the ARIA tree exposed 72 | 1,677 | 3,346 | 1,692 | 72 / 72 | 49.4% |
| github.com/…/issues | 8,287 | 12,375 | 2,976 | 83 / 118 | 76.0% |
| en.wikipedia.org article | 7,585 | 12,958 | 3,060 | 132 / 185 | 76.4% |
| news.ycombinator.com30 identical link 'upvote', 30 identical link 'hide' | 10,345 | 12,684 | 2,350 | 118 / 220 | 81.5% |
| developer.mozilla.org | 4,068 | 5,934 | 1,677 | 59 / 87 | 71.7% |
Mean 71.0% fewer tokens than the snapshot a model actually gets. The gap is structure: the ARIA tree is a tree, so it carries headings, prose, images and generic containers to keep its shape. ZeroDOM emits a flat list — an agent choosing what to click doesn't need the ancestry of the thing it clicks.
Five tools. One in-memory selector map. No page re-reads.
The server keeps one Chromium session and holds node_id → selector server-side. Every action re-reads the page in place, so a click's result comes back in the same turn and node ids never go stale.
Navigate, then return the compact graph. A navigation renumbers every id, so this is the only call that returns the page whole.
Re-read the live DOM without navigating. Re-navigating wipes typed state; this preserves it — measured on Hacker News.
Answer “where is the dispatch button?” with one line instead of the whole graph. The agent loop stops re-reading what it already has.
Click, then return what changed as a structural diff. Accepts whatever the model saw — 03, [03] and node_03 all resolve.
Type into a node and return the same diff. Filling a field that changes nothing structurally costs almost nothing to report.
The saving compounds
It is the difference between an agent spending the full graph on every one of twenty actions and spending it once. A diff is only ever a reduction, never a loss.
+ [18] button 'Confirm dispatch'
− [17] button! 'Recall (in transit)'
~ [07] input* 'Assigned driver' '' → 'A. Reyes'ARIA
link "upvote" × 30 — get_by_role throws
link "hide" × 30 — which story?
ZeroDOM
[42] a 'upvote' → unique by constructionWhat ZeroDOM deliberately does not do.
Known and worth knowing before you build on it. Every one of these is a design decision with a reason, not a roadmap item we're hiding.
Closed shadow roots are unreachable
Open roots are parsed and light-DOM selectors are scoped against them. A root attached with {mode: 'closed'} is hidden from every API, including Playwright's, so nothing can enumerate it.
Iframes aren't traversed
Each frame is a separate document; ZeroDOM parses the top one. Payment fields, embedded editors and consent gates typically live in an iframe and won't appear.
Visibility is read from markup, not layout
Hiding is detected on the element itself — [hidden], aria-hidden, type="hidden", inline display:none. A stylesheet rule can't be seen, so <div class="hidden"> is emitted as if visible. No reflow, no layout pass, no latency cost.
Pure middleware — 0% bot-detection footprint
ZeroDOM operates on a Page you already control and never fetches anything, so it has no anti-bot surface to defeat and makes no claim to bypass one. If your agent drives an authenticated Chrome session, Cloudflare and Akamai were satisfied before ZeroDOM ran.
Canvas and WebGL have nothing to parse
Figma-style surfaces draw their controls as pixels — there is no element to emit. Screenshot-based computer use is the right tool there, not this.
Nothing waits for the page to finish thinking
from_page and zerodom_read_page snapshot the DOM at call time. An SPA still fetching its content returns a graph of the shell. Wait for your own condition first, then parse.
Labels come from the page, and the page is written by someone else. A hostile site can name a button so that it reads as an instruction to whatever model you send the graph to. Every tool that shows a model a web page has this problem — ARIA snapshots, raw HTML, screenshots fed to a vision model. ZeroDOM doesn't add to it, and helps a little by never handing the model a selector or URL it can act on directly. Treat every label as untrusted input, and keep the decision to click on your side.
Frequently asked questions
The short answers, with the number that backs each one. Everything here is reproducible from the repo.
How do I reduce token usage in a browser agent?
Stop sending the agent the page structure and send it only the actions. ZeroDOM replaces Playwright's ARIA snapshot with a flat interaction graph — one line per clickable or fillable element — which measured 71% fewer tokens across five live pages, and holds a flat ~10 tokens per actionable node instead of the accessibility tree's 23–70.
What is ZeroDOM?
ZeroDOM is an open-source MCP server and Python library that converts a web page into a token-optimized interaction graph for AI browser agents. It parses the DOM deterministically with lxml in 10–30ms, emits one line per actionable node, and keeps the CSS selector for each node on your side so it never enters the model's context window.
How is ZeroDOM different from Playwright MCP?
Playwright MCP sends the model an ARIA accessibility snapshot, which is a tree and therefore carries headings, prose, images and generic containers to keep its shape. ZeroDOM sends a flat list of actions only, which measured 71.0% fewer tokens on average, and gives every node a unique id — on Hacker News, 102 of 220 actionable nodes are not uniquely addressable by (role, name), so a snapshot-driven agent cannot say which story to upvote.
How many tokens does ZeroDOM actually save?
Against Playwright's aria_snapshot(mode="ai"), the snapshot a model actually receives, ZeroDOM saved 49.4% on Airbnb, 76.0% on GitHub issues, 76.4% on Wikipedia, 81.5% on Hacker News and 71.7% on MDN — a mean of 71.0%. Against raw HTML the mean saving is 93.1%, with the worst page at 80.4%. All figures were measured on live pages on 2026-08-03 with tiktoken cl100k_base and are reproducible from the repo.
Does ZeroDOM send my page data to an API?
No. The parse is local and deterministic with no LLM in the loop — lxml in, graph out, identical output every run. There is no telemetry, no API key, no account and no storage; the only network traffic is the page you pointed it at, and nothing about your page reaches a model until you send the graph to one.
Does ZeroDOM work with Claude Desktop and Cursor?
Yes. ZeroDOM ships an MCP server that runs with uvx --from zerodom zerodom-mcp, configured in claude_desktop_config.json for Claude Desktop or .cursor/mcp.json for Cursor. It exposes five tools: zerodom_parse_url, zerodom_read_page, zerodom_find, zerodom_click_node and zerodom_fill_node.
Does ZeroDOM bypass bot detection or CAPTCHAs?
No, and it makes no claim to. ZeroDOM is pure middleware over a Playwright Page you already control and never fetches anything itself, so it has a 0% bot-detection footprint — if your agent drives an authenticated Chrome session, Cloudflare and Akamai were satisfied before ZeroDOM ran.
Can ZeroDOM read iframes, shadow DOM and canvas apps?
Open shadow roots are parsed and their selectors are correctly scoped; closed shadow roots, iframes and canvas-rendered controls are not. A closed root is hidden from every API including Playwright's, each iframe is a separate document that ZeroDOM does not traverse, and a canvas app draws its controls as pixels so there is no element to emit.
Is ZeroDOM free and open source?
Yes. ZeroDOM is Apache 2.0 licensed, published on PyPI as zerodom, and installable with pip install zerodom on Python 3.10 or newer. The browser-backed features additionally need playwright install chromium.
How does ZeroDOM stop an agent from re-reading the same page?
Two of its five tools exist for exactly that. zerodom_find returns only the nodes matching a phrase — 7 tokens instead of the full 229-token graph on the demo page — and actions return a structural diff (+ appeared, − gone, ~ value changed) rather than re-listing every node, so the full graph is spent once instead of on every one of twenty actions.
Stop paying for the DOM.
We're onboarding a small number of teams running browser agents in production. Bring your hardest page — the one where the ARIA snapshot eats your context window.
pip install zerodom · Apache 2.0 · no telemetry