> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Render a page, read it, act on what it read

From a sandbox: render captures a pricing page, the node's model reads the image and names a control, and vision presses it and reads the new price.

Three products in a row, on the node that serves the model. Code in a sandbox asks render for a screenshot of the
demo app's billing page, hands the image to `b3os/qwen3.5-9b` through `image_url`, and asks it two things: which
control switches the prices to yearly billing, and what the Business plan costs as shown. Vision then opens the page,
presses the control the model named, and reads the Business price again. Each step is timed inside the sandbox.

The sandbox holds no key. Its grant (`api.scopes`) lets the code inside call render, infer and vision on
`http://127.0.0.1:7700`, and the model preset (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `B3OS_MODEL`) is in its
environment. The model reads the image from the screenshot's artifact URL, so the image never passes through the
sandbox.

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

## Run it

**TypeScript**

Inside the sandbox (Node 24 in the `base` template):

```ts
const API = "http://127.0.0.1:7700";
const PAGE = "https://beta.b3os.org/demo/billing/";
const post = async (url: string, body: unknown, auth?: string): Promise<any> =>
  (
    await fetch(url, {
      method: "POST",
      headers: { "content-type": "application/json", ...(auth ? { authorization: auth } : {}) },
      body: JSON.stringify(body),
    })
  ).json();

// 1. render: the page as an image
const shot = await post(`${API}/v1/render/screenshots`, {
  url: PAGE,
  viewport: { width: 1280, height: 800 },
  format: "jpeg",
});

// 2. infer: the model reads the image
const read = await post(
  `${process.env.OPENAI_BASE_URL}/chat/completions`,
  {
    model: process.env.B3OS_MODEL,
    temperature: 0,
    chat_template_kwargs: { enable_thinking: false },
    response_format: {
      type: "json_schema",
      json_schema: {
        name: "pricing_page",
        schema: {
          type: "object",
          properties: { toggle: { type: "string" }, business_monthly: { type: "string" } },
          required: ["toggle", "business_monthly"],
        },
      },
    },
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "This is a screenshot of a pricing page. Give the visible text of the control that switches the prices to yearly billing, and the Business plan's price exactly as the image shows it.",
          },
          { type: "image_url", image_url: { url: shot.artifacts[0].url } },
        ],
      },
    ],
  },
  `Bearer ${process.env.OPENAI_API_KEY}`,
);
const seen = JSON.parse(read.choices[0].message.content);
await fetch(`${API}/v1/render/screenshots/${shot.id}`, { method: "DELETE" }); // read; no need to keep it 7 days

// 3. vision: press what the model named, then read the page
const session = await post(`${API}/v1/vision/sessions`, {});
const done = await post(`${API}/v1/vision/sessions/${session.id}/act`, {
  actions: [
    { kind: "navigate", url: PAGE },
    { kind: "click", target: { role: "button", name: seen.toggle } },
    { kind: "markdown", only_main_content: true },
  ],
});
await fetch(`${API}/v1/vision/sessions/${session.id}`, { method: "DELETE" });
console.log(seen, done.results[2].markdown);
```
**curl**

Create the sandbox with the grant, then run the script above in it as `agent.mjs`:

```bash
API=https://beta-api.b3os.org
AUTH="Authorization: Bearer $B3OS_API_KEY"

SBX=$(curl -s "$API/v1/sandboxes" -H "$AUTH" -H "Content-Type: application/json" -d '{
  "template": "base", "timeout_s": 300,
  "api": {"scopes": ["render:run", "render:write", "infer:run", "vision:write", "vision:run"], "spend_cap_usd": 1}}' | jq -r .id)
curl -s -X PUT "$API/v1/sandboxes/$SBX/files?path=/workspace/agent.mjs" -H "$AUTH" \
  -H "Content-Type: application/octet-stream" --data-binary @agent.mjs > /dev/null
curl -s "$API/v1/sandboxes/$SBX/exec" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"cmd": "node /workspace/agent.mjs", "timeout_ms": 60000}' | jq -r .stdout
curl -s -X DELETE "$API/v1/sandboxes/$SBX" -H "$AUTH" | jq .status
```
**Python**

The same three calls from outside the node, with your key. Each one then crosses your link and the edge:

```python
import json
import os
import requests

API = "https://beta-api.b3os.org"
PAGE = "https://beta.b3os.org/demo/billing/"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"

shot = s.post(f"{API}/v1/render/screenshots", json={
    "url": PAGE, "viewport": {"width": 1280, "height": 800}, "format": "jpeg"}).json()
read = s.post(f"{API}/v1/infer/chat/completions", json={
    "model": "b3os/qwen3.5-9b", "temperature": 0, "chat_template_kwargs": {"enable_thinking": False},
    "response_format": {"type": "json_schema", "json_schema": {"name": "pricing_page", "schema": {
        "type": "object", "required": ["toggle", "business_monthly"],
        "properties": {"toggle": {"type": "string"}, "business_monthly": {"type": "string"}}}}},
    "messages": [{"role": "user", "content": [
        {"type": "text", "text": "This is a screenshot of a pricing page. Give the visible text of the control that "
                                 "switches the prices to yearly billing, and the Business plan's price exactly as "
                                 "the image shows it."},
        {"type": "image_url", "image_url": {"url": shot["artifacts"][0]["url"]}}]}],
}).json()
seen = json.loads(read["choices"][0]["message"]["content"])

session = s.post(f"{API}/v1/vision/sessions", json={}).json()
try:
    done = s.post(f"{API}/v1/vision/sessions/{session['id']}/act", json={"actions": [
        {"kind": "navigate", "url": PAGE},
        {"kind": "click", "target": {"role": "button", "name": seen["toggle"]}},
        {"kind": "markdown", "only_main_content": True}]}).json()
    print(seen, done["results"][2]["markdown"][:400])
finally:
    s.delete(f"{API}/v1/vision/sessions/{session['id']}")
```
**MCP**

On the remote [B3OS MCP server](/sdks/mcp), ask your agent:

```text
Take a 1280x800 screenshot of https://beta.b3os.org/demo/billing/. Look at the image and tell me which control
switches the prices to yearly billing and what the Business plan costs as shown. Then open the page in a browser
session, press that control, and tell me the Business price it shows now. Close the session when you are done.
```

The agent calls `render_screenshot`, then reads the image itself, then `vision_create_session`, `vision_act` and
`vision_close_session`.

The recipe's run tries the control as a button, then as a link, then as any element with that text, and checks the
price the page shows after the press. The model's two answers are free text from a page we publish, so a run whose
answers or page change waits for a person's review before this page shows it.

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

## Output of the last green run

**Outputs**

```json
{
  "page_url": "https://beta.b3os.org/demo/billing/",
  "model": "b3os/qwen3.5-9b",
  "render_ms": 738.5,
  "image": {
    "width": 1280,
    "height": 800,
    "bytes": 74091
  },
  "screenshot_deleted": true,
  "model_ms": 1153.6,
  "prompt_tokens": 1051,
  "toggle": "Annual",
  "price_read": "$48 per month",
  "act_ms": 240.4,
  "pressed": true,
  "price_after": "$40",
  "read_sha256": "aa46f02b258763e5c79ea3632fbf98f69f00b697df2ac480fe2820e961a56b09",
  "session_closed": true,
  "total_ms": 2165.6,
  "create_ms": 432,
  "exec_ms": 2346.3,
  "deleted": true
}
```
**Command**

```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-03 from commit `a0816c07`, label `live-recorded` (recorded from production). Case `recipes/render-read-act`.