An agent loop with the model, the browser and the agent on one node. Grant a sandbox infer:run, vision:write and vision:run, and the code inside it can open a browser session and call the model with no key: the sandbox gets OPENAI_BASE_URL, a placeholder OPENAI_API_KEY and B3OS_MODEL, and B3OS answers at http://127.0.0.1:7700. The agent (agent.mjs in the recipe) opens the demo app's billing page and asks the model what the Business plan costs when billed yearly. The page shows monthly prices first, so the model has to press Annual before it can answer. It has three tools: read_page (the page's main content as Markdown), click (a control by its visible name) and answer. Each turn is one model call and one tool call:

js
const r = await ask(); // tools: read_page, click, answer; tool_choice "required"const call = r.json.choices[0].message.tool_calls[0];if (call.function.name === "click") { const { name } = JSON.parse(call.function.arguments); await post(`${API}/v1/vision/sessions/${sid}/act`, { actions: [{ kind: "click", target: { role: "button", name } }, { kind: "markdown", only_main_content: true }], });}

After the model answers, the agent's own code reads the page (which period is pressed, the Business price) so the answer is checked against the page and not against the model.

Check

Verified 2026-11-02: this recipe's last run passed against https://beta-api.b3os.org, label live-recorded (recorded from production), from commit a5c2b1a7. The recipe harness runs it again, and a failed run takes this page out of the cookbook.

Run it

Each tab creates the sandbox with the grant, copies agent.mjs in, runs it and deletes the sandbox. The agent closes its browser session itself, also when a turn fails.

bash
SBX=$(curl -s https://beta-api.b3os.org/v1/sandboxes \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"template": "base", "timeout_s": 300, "api": {"scopes": ["infer:run", "vision:write", "vision:run"], "spend_cap_usd": 1}}' \ | jq -r .id)curl -s -X PUT "https://beta-api.b3os.org/v1/sandboxes/$SBX/files?path=/tmp/agent.mjs" \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/octet-stream" \ --data-binary @agent.mjscurl -s "https://beta-api.b3os.org/v1/sandboxes/$SBX/exec" \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cmd": "node /tmp/agent.mjs", "timeout_ms": 60000}' | jq -r .stdoutcurl -s -X DELETE "https://beta-api.b3os.org/v1/sandboxes/$SBX" -H "Authorization: Bearer $B3OS_API_KEY"

model_ms and tool_ms below are each turn's model call and browser action, measured inside the sandbox, on the same machine as the model and the browser. A buffered exec waits at most 60 seconds; a longer agent streams its output or runs in the background. The demo app is a fictional workspace with made-up prices.

Owner: Docs. Products: Docs, Sandboxes, Infer, Vision.

Output of the last green run

bash
$ node run.mjs# exit 0

The outputs this recipe publishes (its recipe.json publish.outputs), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.

Captured 2026-11-02 from commit a5c2b1a7 , label live-recorded (recorded from production). Case recipes/sandbox-agent-browser .

Ask a question... ⌘I