A browser your AI can afford.
Record a task once. Run it on a thousand rows for the cost of one.
npm i -g pixelpipixelpi lets any AI model use a real web browser, and it reads each page in about 2,000 tokens instead of 180,000. Then it goes further: once a task works, you record it, and pixelpi repeats those exact steps with no model in the loop, across a whole dataset, in parallel, self-healing when a page changes. It is pi, the minimal coding agent, applied to the browser: six tools, raw Chrome DevTools Protocol, any model, your key.
- Read any page in ~2,000 tokens, 37x to 100x fewer than raw DOM.
- Record a solved task once; replay it with no model, deterministically, for free.
- Run a recorded task across a CSV or JSONL in parallel, about 0 tokens per row.
- Self-heals: when a page changes, the model fixes one step and every other row reuses it.
- Callable from code, readable by other AI agents (typed SDK plus a clean
--jsoncontract).
A normal browser agent does 5,000 inputs as 5,000 full loops. pixelpi does one recorded run plus 4,999 free replays. The model thinks once; the rest is nearly free.
npm i -g pixelpi
pixelpi auth # set provider + key (any model)
pixelpi "find the top story on Hacker News" # run a task live
pixelpi "search Hacker News for rust" --record hn # record it, then name the input
pixelpi run hn --over queries.csv --concurrency 8 # run it across a dataset, in parallelNo key is needed to reproduce the token numbers or to replay a recorded trace. A key is only for the live model loop, and pixelpi works with any provider.
look() versus a raw-DOM dump, across the sites WebVoyager tests on. Full table and a reproducible script (no key needed) are in bench/:
| Site | look() |
raw DOM | factor |
|---|---|---|---|
| Coursera | 1,997 tok | 202,892 tok | 101.6× |
| GitHub | 1,955 tok | 146,787 tok | 75.1× |
| Apple | 2,254 tok | 96,507 tok | 42.8× |
| Hugging Face | 1,932 tok | 45,300 tok | 23.4× |
| ArXiv | 1,588 tok | 10,652 tok | 6.7× |
look() holds around 2k tokens whatever the page weighs, while the raw DOM keeps growing. The heavier the site, the bigger the win.
| pixelpi | Playwright MCP | Chrome DevTools MCP | |
|---|---|---|---|
| Tools in context | 6 | 21 | 31 |
| Tool-def + prompt tokens | ~1,055 | ~13,700 | ~18,000 |
| Page representation | a11y tree (bounded) | mixed | mixed |
| Substrate | raw CDP (no Playwright) | Playwright | CDP |
| Self-extension | agent writes JS skills at runtime | no | no |
| Replay | record once, replay with 0 tokens | no | no |
| Parallel fan-out | record once, run a dataset for ~0 tokens/row | no | no |
Bulk form submissions, scraping at scale, price, stock, or account checks across a list, nightly portal exports, or giving your own AI agent a browser tool it can afford and call.
Save a solved run as a trace, then replay it with no model in the loop. The first run is the compile step; every replay is the binary: free, deterministic, and fast.
pixelpi "find the top story on Hacker News" --record hn-top # solve once, save a trace
pixelpi replay hn-top # rerun it, no model, 0 tokens
pixelpi replay hn-top --heal # repair one step if the page driftedTraces key on the accessibility role and name of each element, not CSS selectors or coordinates, so they survive most layout changes. A bare name lives in ~/.pixelpi/traces/; pass a path (or a name ending in .json) to keep a trace inside a repo. Strict replay needs no API key and exits 3 on drift, naming the step that no longer matches. --heal re-derives just that step with the model and rewrites the trace, so it self-corrects over time. Replay reproduces actions, not intent: it shines for stable, repeated flows.
A parametrized trace is a function. Record it once with an example input, then run it across a list:
pixelpi "search Hacker News for rust" --record hn # then name "rust" as the input q
pixelpi run hn --query rust # one input
pixelpi run hn --over queries.csv --concurrency 8 # map over a CSV/JSONL, in parallelEach row runs in its own headless Chrome, bounded by --concurrency (default 4). Outcomes stream to a JSONL file (--out), --resume skips rows already done, and the first row runs alone as a warm-up so a single --heal repair benefits every other row. A 5,000-row job costs one model run plus, at most, a handful of repairs.
Every trace is an introspectable function. describe shows its inputs and output:
pixelpi describe hn # human card: task, inputs, output, usage
pixelpi describe hn --json # {"type":"description","params":[...],"output":{...}}Under --json, every command emits one NDJSON stream (progress, results, and errors as {"type":"error","code":...}), so an AI agent can drive pixelpi and parse a single clean contract.
Every run uses a fresh, disposable Chrome profile by default. To stay logged in across runs, use a persistent profile:
pixelpi login https://github.com # sign in once, press Enter to save
pixelpi --profile "check my GitHub notifications" # reuse the saved session, headless--profile uses ~/.pixelpi/profile; --profile=<dir> uses a custom one. pixelpi finds Chrome automatically on macOS, Linux, and Windows; set PIXELPI_CHROME=/path/to/chrome to override.
look · act · fill · nav · eval · store
look(mode?, filter?): compact, ref-indexed accessibility snapshot. Theread.act(ref, op, value?): mutate the page by stable ref via trusted CDP input events. Thewrite.fill(fields[]): batched form fill in one call.nav(action, arg?): navigate, tabs,waitfor.eval(fn, args?, opts?): arbitrary JS in the page realm. The escape hatch, thebashof the browser.store(action, key?, value?): durable host-side JSON KV. The filesystem.
Elements are addressed by stable ref, not CSS or coordinates: cheap, deterministic, resilient to layout churn. Everything else composes from eval; the agent writes its own higher-level tools as JSON skills at runtime, and only each skill's one-line description enters the prompt.
Load a saved trace as a callable function, no model or API key required:
import { loadTrace } from "pixelpi";
const hn = loadTrace("hn"); // by name (home library) or path
console.log(hn.describe()); // { params, output, ... }
const r = await hn({ query: "rust" }); // run once -> { ok, output }
const rs = await hn.over(rows, { concurrency: 4 }); // map over a dataset, results in input orderOr drive the full agent loop, or the six primitives against raw CDP with no model. More in examples/.
The model is the harness now, so you expose the substrate's irreducible primitives and let the agent compose the rest. This is the bet pi made for the terminal, applied to the browser. See docs/how-it-works.md for the moving parts (why six tools, why raw CDP, why no MCP).
Substrate (look/eval) is validated live against real sites. The agent loop, guards, stores, replay, run, and provider adapters are unit-tested (216 tests, mock provider, no network in tests). The full model-to-browser loop runs once you supply an API key. Requires Node >= 20 and Google Chrome (macOS, Linux, or Windows). Pre-1.0 and moving fast.
Issues and PRs welcome. Run pnpm install && pnpm build && pnpm test before opening a PR. See CONTRIBUTING.md.
MIT © 2026 Harsh Joshi