Getting Started
Why this exists
Every browser vendor has a different idea of what "get me this page" means. Steel does it in one POST. Kernel wants a live browser first and then runs Playwright inside it. Browserbase fetches without a session but hands you a CDP URL for anything else. Anchor doesn't scrape at all.
That's fine for you, you read the docs once. A model reads them never. So @agntn/browsers puts one scrape() over all of them, opens and closes the session when a vendor needs one, and lets every backend say what it can do through capabilities(). Same thing in TypeScript, in the terminal and in an agent.
Install
pnpm add @agntn/browsers
Node.js 26 or newer.
First call
Playwright runs Chromium on your machine, so it needs no key. It takes the Chrome or Chromium already on your PATH. Without one, npx playwright-core install chromium gets it.
import { create } from "@agntn/browsers";
const provider = await create("playwright");
const page = await provider.scrape("https://example.com");
page.title; // "Example Domain"
page.text; // "This domain is for use in documentation examples…"
create() is async on purpose. That's the moment the provider module gets imported, so importing the package costs nothing and a bundler splits every provider into its own chunk.
Who scrapes, and how
| Needs | ||||
|---|---|---|---|---|
| Steelsteel | one call | 4 / 13 | STEEL_API_KEY | 12,348 chars of html |
| Browserbasebrowserbase | one call | 3 / 13 | BROWSERBASE_API_KEY | 209 chars of markdown |
| Kernelkernel | in a session | 6 / 13 | KERNEL_API_KEY | 12,363 chars of html |
| Browserlessbrowserless | one call | 8 / 13 | BROWSERLESS_API_KEY | 12,364 chars of html |
| Hyperbrowserhyperbrowser | one call | 7 / 13 | HYPERBROWSER_API_KEY | 167 chars of markdown |
| Anchoranchor | no scrape | 3 / 13 | ANCHOR_API_KEY | refused, no page |
| Cloudflarecloudflare | one call | 10 / 13 | CF_API_TOKEN and CF_ACCOUNT_ID | 886 chars of markdown |
| Playwrightplaywright | one call | 9 / 13 | no key | 917 chars of text |
A cloud provider needs its key in the environment, STEEL_API_KEY for Steel and so on. Cloudflare wants two, a token and an account ID. Providers has the full list and a page per vendor.
Let it pick
import { create, resolveProvider } from "@agntn/browsers";
const provider = await create(resolveProvider());
resolveProvider() walks the registry in order and takes the first provider whose key is set. Steel if you set STEEL_API_KEY. Playwright if you set nothing, because it's last and never needs a key. Pass a name and it checks that one instead, and throws AuthError when its key is missing, not something else quietly.
Where next
- Scrape for what comes back and why it isn't the same everywhere.
- CLI if you live in a terminal.
- MCP, Pi and OMP to hand all of this to a model.