Tensor LabsTENSORLABS

Giving your agents a browser without giving each one a Chrome

Smooth, a serverless browser agent API, shipped in the May-June 2026 open-source-adjacent tooling wave.

June 12, 20267 min read10 sectionsBy Tensor Labs
Giving your agents a browser without giving each one a Chrome

Introduction

Smooth, a serverless browser agent API, shipped in the May June 2026 open source adjacent tooling wave. It exists to solve a quiet cost problem: every AI agent that needs the web tends to spin up its own headless Chrome through Playwright or Puppeteer, and those processes are heavy, slow to start, and a pain to scale. Smooth replaces the per agent browser with a single API call. It leads the WebVoyager benchmark at 92% while running 5x faster and 7x cheaper than Browser Use. This tutorial wires Smooth into an LLM agent as a tool and compares it against the spin up your own Chrome approach.

What Smooth is

Smooth is a hosted browser agent. You send it a natural language task, it drives a real browser in the cloud, and it returns a structured result plus a live URL you can watch. The browser is not your problem anymore. There is no Chromium binary to install, no no-sandbox flags to debug, no zombie processes leaking memory in your container.

Under the hood Smooth uses small, efficient models tuned for web navigation, which is why it is both faster and cheaper than agent frameworks that drive a full browser with a frontier model on every click. You are renting navigation as a service instead of building it.

Setup

code
python3 -m venv .venv && source .venv/bin/activate
pip install smooth openai
# Get a key from smooth.sh; it looks like cmzr-...
export SMOOTH_API_KEY="cmzr-YOUR_API_KEY"
export OPENAI_API_KEY="sk-..."

Your first browser task in four lines

The whole API is run(). You hand it a task in plain English, and you get back a handle with a live URL and a result.

python
import os
from smooth import SmoothClient
smooth_client = SmoothClient(api_key=os.environ["SMOOTH_API_KEY"])
task = smooth_client.run(
    "Go to Google Flights and find the cheapest flight from London to Paris today"
)
print(f"Live URL: {task.live_url()}")
print(f"Agent response: {task.result()}")

live_url() returns a watchable session you can drop into a dashboard while the task runs. result() blocks until the browser agent finishes and returns the answer. No selectors, no waits, no retry loops around flaky elements. That logic lives inside Smooth.

Making it a tool your LLM can call

A browser is only useful to an agent if the agent decides when to use it. Here Smooth becomes a single tool in an OpenAI tool-calling loop. The model reasons; when it needs the live web, it calls browse.

python
import json, os
from openai import OpenAI
from smooth import SmoothClient
oai = OpenAI()
smooth_client = SmoothClient(api_key=os.environ["SMOOTH_API_KEY"])
def browse(task: str) -> str:
    """Run a web task in a Smooth serverless browser and return the result."""
    return smooth_client.run(task).result()
    TOOLS = [{
        "type": "function",
        "function": {
            "name": "browse",
            "description": "Perform a task on the live web using a browser agent.",
            "parameters": {
                "type": "object",
                "properties": {
                    "task": {"type": "string", "description": "A plain-English web task"}
                },
                "required": ["task"],
            },
        },
    }]
    def run_agent(user_goal: str) -> str:
        messages = [{"role": "user", "content": user_goal}]
        while True:
            resp = oai.chat.completions.create(
                model="gpt-4o-mini", messages=messages, tools=TOOLS
            )
            msg = resp.choices[0].message
            if not msg.tool_calls:
                return msg.content
                messages.append(msg)
                for call in msg.tool_calls:
                    args = json.loads(call.function.arguments)
                    result = browse(args["task"])
                    messages.append({
                        "role": "tool",
                        "tool_call_id": call.id,
                        "content": result,
                    })
                    print(run_agent("Find the current price of a Framework 13 laptop and tell me if it is in stock."))

The agent never touches a browser object. It emits a task string, browse hands it to Smooth, and the result comes back as tool output. You can run a hundred of these concurrently and your process stays a thin Python loop, because the browsers live in Smooth's infrastructure, not yours.

What you are actually saving

The Playwright-per-agent pattern looks cheap until you scale it. Each concurrent task is a Chromium process: a few hundred MB of RAM, a cold-start penalty, and a babysitter for crashes. Run fifty in parallel and you are provisioning a fleet to host browsers, not to do work.

python
# The pattern Smooth replaces - one heavyweight browser per task:
    from playwright.sync_api import sync_playwright
    def browse_local(url: str) -> str:
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True) # cold start, ~hundreds of MB
            page = browser.new_page()
            page.goto(url)
            text = page.inner_text("body")
            browser.close() # and you must not forget this
            return text

That function works for one task on your laptop. At fifty concurrent tasks it becomes an infrastructure project: process limits, memory pressure, and a browser.close() you will eventually forget on an error path and leak. (The headless Chrome you forgot to close is still running. It will be running tomorrow.) Smooth's pitch is that browser orchestration is undifferentiated heavy lifting, and the WebVoyager numbers (92%, 5x faster, 7x cheaper than Browser Use) are the receipt.

Running a hundred at once

The cost argument is only real if you can fan out, and this is where the serverless model pays. Smooth tasks are independent, so run them concurrently with a thread pool. Each run().result() call blocks, but the browsers live in Smooth's infrastructure, so the parallelism costs your machine almost nothing.

python
import os
from concurrent.futures import ThreadPoolExecutor
from smooth import SmoothClient
smooth_client = SmoothClient(api_key=os.environ["SMOOTH_API_KEY"])
tasks = [
    "Find the price of the Framework 13 on framework.com",
    "Find the price of the Dell XPS 13 on dell.com",
    "Find the price of the MacBook Air M4 on apple.com",
]
def run_task(task: str) -> str:
    return smooth_client.run(task).result()
    with ThreadPoolExecutor(max_workers=20) as pool:
        results = list(pool.map(run_task, tasks))
        for task, result in zip(tasks, results):
            print(f"{task}\n -> {result}\n")

Twenty browser sessions run in parallel and your host stays idle. The Playwright version of this is twenty Chromium launches fighting over the RAM on one machine, which is exactly the fleet you were trying not to operate.

Getting structured data back

result() returns text by default. For a pipeline you want fields, not prose, so describe the output shape in the task and parse what comes back. Because Smooth drives a real browser with a model, you ask for the structure in plain English instead of writing selectors that shatter the next time the page ships a redesign.

python
import json
from smooth import SmoothClient
smooth_client = SmoothClient(api_key=os.environ["SMOOTH_API_KEY"])
task = smooth_client.run(
    "Go to news.ycombinator.com, read the top 5 stories, and return a JSON "
    "array of objects with keys 'title' and 'points'. Return only the JSON."
)
data = json.loads(task.result())
for story in sorted(data, key=lambda s: s["points"], reverse=True):
    print(f"{story['points']:>4} {story['title']}")

The brittle part of scraping was never the navigation. It was the selectors that assumed the DOM would hold still. Moving the structure into the instruction is what makes this survive a redesign.

Watching a task while it runs

The live_url() handle is not just for logs. It is a real, watchable browser session, which matters the moment a task does something you did not expect and you need to see why. Drop it into an internal dashboard, or just open it during development while result() is still running

python
task = smooth_client.run("Book the cheapest refundable hotel in Lisbon for next weekend")
print(f"Watch it live: {task.live_url()}") # open this while result() is still pending
print(task.result())

When a browser agent makes a surprising choice, text logs tell you what it concluded. The live URL tells you what it actually saw on the page. For debugging the long tail of weird, half-broken websites, that difference is the entire game, and it is the thing a headless Playwright run hides from you.

When to use Smooth, and when to keep Playwright

Use Smooth when agents need the open web at scale, when tasks are described as goals rather than scripts, and when you would rather not run a browser fleet. The cost and reliability case is strongest exactly where the local approach is most painful: many concurrent, unpredictable navigation tasks.

Keep Playwright when you need deterministic, scripted control of a specific page (scraping one known site on a schedule, end-to-end testing your own app), when the data cannot leave your network, or when you are doing one task on one machine and the whole serverless argument is moot. A hosted browser you pay per task is the wrong tool for a cron job that hits the same URL every night.

For a decade the answer to "my code needs a browser" was "install Chromium and pray." Smooth's bet is that the browser should be somebody else's process. If your agents are already drowning your containers in headless Chrome, that bet is worth measuring against your own bill.