Blog
Browser vs API for AI Agents: The Hybrid Case
Agent infrastructure has a recurring argument. One side wants agents to call APIs: structured, cheap, deterministic. In that view, browser automation is a stopgap until real endpoints exist. The other side wants agents to drive browsers: the web is the universal interface, and an agent that can use a browser can use anything. Both sides identify the other's weakness and understate their own.
We treat that split as false. The decision unit is not the agent or the site. It is the individual screen and action. The right architecture maps a site once, then selects the transport for each action: a real browser when the screen requires one, and a direct server-side request when it does not. That is how we build Rindler. This essay explains why.
What pure API misses: most of the web
The API-first position depends on one condition: an API exists. For the long tail of the web, it does not. Rindler targets that tail: legacy portals, private SaaS dashboards, government systems, and multi-step forms, the websites that were never given a reliable API. These are the sites where real work happens, and nobody is retrofitting endpoints onto them. This is not limited to shopping or to one vertical. It applies to any website that has a screen but no contract.
"Just fetch the page" does not replace an API. Many sites respond to a direct HTTP fetch with a 403, a JavaScript-only shell with no content, or a bot-challenge page. On a JavaScript-heavy site, the data your agent needs is not in the response. It exists only after a real browser renders the page. An API-only stack does not degrade gracefully in that case. It stops at the edge of the documented web, and the documented web is the small part.
What pure browser costs: tokens and breakage
The browser-maximalist position pays for universality twice.
First, it pays in tokens. A raw fetch of a typical page, such as listings, a detail view, a dashboard, or search results, is typically 50,000+ tokens of nav, footer, sidebar, and tracking noise around the actual data. The structured version of the same answer is typically a few hundred tokens of JSON. An agent that re-reads the DOM on every step spends its context window on markup instead of the task.
Second, it pays in breakage. Tools that drive or scrape a page live on every run fail in different ways, but they fail. A script chasing CSS selectors breaks when the site redesigns. An LLM re-reading the DOM drifts with every popup and A/B-tested layout. The property you need is determinism: the same call against the same site returns the same response shape across runs, independent of DOM changes.
The gap is measurable. On one live grocery benchmark, the mapped version of the task finished in 98 seconds instead of 407 and cost $0.26 instead of $1.54. Same task, same site, same outcome. The difference was what the agent had to read and how many times it had to guess.
Transport is a per-action decision
Inside Rindler, this is an engineering invariant: browser-worker vs server-proxy per the screen/action recipe, not habit.
Some actions need a browser. dispatch_action is the one tool in our server that changes external site state: it can submit forms, orders, messages, and applications. Those actions run against a live browser session that start_session provisions and close_session releases, because the site's own JavaScript, session state, and rendered UI are load-bearing for the action.
Other actions do not need that path. extract_document reads a linked PDF by fetching its URL directly: anonymously, sending no cookies, so it reads only what a signed-out visitor could read. No browser spins up, because a PDF has no accessibility tree and no rendered page worth driving; extract_content, the tool that reads rendered screens, cannot read one at all. Same product, same session model from the agent's point of view, different transport underneath, selected by what the action needs rather than by an architecture camp.
Choosing per action instead of per architecture is the core move. The browser stops being a default and becomes a cost you pay only when a screen requires it.
The mapped config is what makes the choice possible
Per-action routing requires known actions. That is what mapping provides. A Rindler site config records each screen a site exposes, the actions valid on it, and their parameter schemas; the agent discovers all of it from available_actions rather than guessing. Every screen exposes structured records through an extraction schema, and raw accessibility output is a fallback used only when no schema is registered, never the normal path. Even a screen's filters are read live from the current page rather than hardcoded, because filters change with context: a marketplace's Vehicles category adds Make, Model, and Year.
Two properties keep the config honest. Every mapping ships only after an automated verifier has passed it against the real site, and configs are self-healing, adapting when a layout changes instead of waiting for someone to rewrite selectors. The catalog is not fixed either: if a site you need is not mapped yet, you can map it yourself on demand.
With that map, routing becomes mechanical. Does this action mutate state? Does this screen require rendering or a signed-in session? If yes, it uses the browser worker. If no, it uses the cheaper server-side path. The recipe answers per screen and per action, so the decision is made at mapping time and verified against the real site, instead of being re-derived by a model during the task.
What your agent sees
This routing does not leak into your agent. It calls the same four tools either way: start_session, dispatch_action, extract_content, close_session. Every response is structured JSON carrying the current screen, persistent context, screen-specific data, and the actions now available. The full action schemas arrive once, when the session starts; dispatch_action responses carry just the action names to save tokens, because your agent already holds the schemas from start_session. Authentication, navigation, retries, and error recovery run server-side. Bot evasion is managed for you. Your agent never touches raw HTML or a CSS selector, and it neither knows nor cares which transport carried the call.
That is the point. Transport is an infrastructure decision. It belongs below the tool boundary, not inside your agent's prompt. An agent that chooses between a fetch and a browser on every step is doing plumbing. An agent given typed actions and structured records is doing the task.
The wrong question and the right one
Browser or API is the wrong question. The right question is narrower: for this screen, for this action, what is the cheapest transport that stays reliable?
A pure-API stack answers once, for everything, and forfeits the long tail of sites that never got an API. A pure-browser stack answers once, for everything, and pays browser prices for data a plain request could carry. A mapped config stops answering once. It answers per action, keeps the answer verified against the real site, and lets each call use the cheapest transport that holds up.
If you want the mechanics, How it works walks the pipeline from mapping to verified config. If you are weighing this against scraping or raw browser automation, Compare puts the categories side by side. The four tools your agent actually calls are documented in Browser MCP.