Reading 1,000 screenshots with a reasoning model now costs about six cents. Send each screenshot to Qwen3.7 Flash as an OpenAI-compatible image_url content part, ask for a strict JSON decision, and run it in a loop. The model is listed on OpenRouter as qwen/qwen3.7-flash at roughly $0.03 per 1M input tokens and $0.13 per 1M output, with a ~1M-token context and text, image, and video input. That price is the whole story: "look at a screen and decide" just became cheap enough to run continuously.

This is a build guide. You'll get a working client call, a queue loop, and the cost arithmetic you can check yourself.

Why the price changes what you build#

When a vision-capable reasoning model cost dollars per million tokens, screen-reading in a loop was a luxury — you rationed it, cached hard, and reserved it for one-off document parsing. At ~$0.03 per 1M input, the math flips. The unit economics of a per-task agent stop being dominated by the model call and start being dominated by whatever you were avoiding: retries, human review, brittle selectors.

The non-obvious insight: cheap multimodal reasoning doesn't just make old vision workloads cheaper — it moves the line of which vision features are worth automating at all. Per-click UI verification and continuous frame monitoring were never worth the per-frame cost. Now they're pennies.

If reading a screen is nearly free, you stop writing fragile scrapers and start letting the agent look.

The price landed in this week's founders' wire. For how it stacks up against the open-weight field, see the best open vision-language model for agents.

The core call#

Qwen3.7 Flash speaks the OpenAI protocol, so any OpenAI-compatible client works — point it at OpenRouter or at Alibaba's DashScope endpoint. Images go in as an image_url content part; for local screenshots you pass a base64 data URL (data:image/png;base64,...), the same format OpenRouter documents for image inputs and Alibaba documents for Qwen-VL.

import base64, json, os
from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",   # or the DashScope compatible-mode URL
    api_key=os.environ["OPENROUTER_API_KEY"],
)

SCHEMA_HINT = (
    "Return ONLY JSON: "
    '{"action": "click|wait|done|escalate", '
    '"target": "<short label or null>", "reason": "<one sentence>"}'
)

def decide(png_bytes: bytes, goal: str) -> dict:
    b64 = base64.b64encode(png_bytes).decode()
    resp = client.chat.completions.create(
        model="qwen/qwen3.7-flash",
        temperature=0,
        max_tokens=200,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text",
                 "text": f"Goal: {goal}\n{SCHEMA_HINT}"},
                {"type": "image_url",
                 "image_url": {"url": f"data:image/png;base64,{b64}"}},
            ],
        }],
    )
    raw = resp.choices[0].message.content
    return json.loads(raw)   # validate before trusting — see pitfalls

Note the text prompt comes before the image — OpenRouter recommends ordering text first. Set temperature=0 for stable decisions, and keep max_tokens small: output is the expensive per-token side, and a JSON verdict needs very little of it.

The loop#

A screen-reading agent is this call wrapped in a bounded loop over a queue of frames. The two rules that keep it cheap and safe: cap the iterations and validate every parse before acting.

def run(frames, goal, max_steps=25):
    for i, png in enumerate(frames):
        if i >= max_steps:
            return {"action": "escalate", "reason": "step cap hit"}
        try:
            d = decide(png, goal)
            assert d["action"] in {"click", "wait", "done", "escalate"}
        except (json.JSONDecodeError, KeyError, AssertionError):
            d = {"action": "escalate", "reason": "bad JSON"}
        if d["action"] in {"done", "escalate"}:
            return d
        act(d)          # your click/scroll executor
    return {"action": "escalate", "reason": "queue exhausted"}

The step cap is your circuit breaker: a runaway visual loop that misreads "loading" as "click me" can otherwise burn frames — cheap per frame, but not free at scale.

The cost math (estimate, shown transparently)#

Image tokens dominate. A screenshot isn't one token — the model tiles it into patches, so a full-resolution capture can cost well over a thousand input tokens on its own, while your JSON reply costs a hundred or so. Here's the arithmetic with clearly labeled assumptions:

# ASSUMPTIONS (estimate — image token counts vary by resolution/model)
IMG_TOKENS   = 1300     # a screenshot downscaled to ~1 megapixel
PROMPT_TOKENS = 200     # goal + schema hint
OUT_TOKENS   = 120      # the JSON decision

IN_RATE  = 0.03 / 1_000_000   # $/input token  (OpenRouter listing)
OUT_RATE = 0.13 / 1_000_000   # $/output token

per_shot = (IMG_TOKENS + PROMPT_TOKENS) * IN_RATE + OUT_TOKENS * OUT_RATE
print(per_shot * 1000)        # -> ~0.0602  ==>  about $0.06 / 1,000 screens

Roughly $0.045 for the inputs and $0.016 for the outputs per 1,000 frames — about six cents, and ~75% of it is the images. That ratio is the whole optimization strategy: shrink the picture, not the prompt.

Pitfalls that actually bite#

Six cents per 1,000 screens is the headline, but the real unlock is that you can now afford to check: verify every click, monitor every frame, read every document that lands. Before, you couldn't.

For the head-to-head on which cheap model to actually pick, see Qwen3.7 Flash vs Gemini 3.6 Flash for vision.