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#
- Image tokens are the bill. Downscale every screenshot before encoding (target ~1 MP; crop to the region you care about). Halving pixels roughly halves your dominant cost.
- Don't trust text as JSON. Request a strict schema,
json.loadsinside atry, and default toescalateon failure. Reasoning models occasionally wrap JSON in prose — validate, don't hope. - Turn reasoning off for simple reads. The Qwen3.7 generation ships with thinking on by default; a "which button is this" decision doesn't need a reasoning trace, and traces are output tokens you pay for. Keep it on only for genuinely ambiguous screens.
- Cap the loop and cache. A step ceiling bounds worst-case spend; hashing identical frames skips redundant calls entirely.
- Measure your real token count. The 1,300-token figure is an estimate — pull the actual
usageoff each response and benchmark it against your own frames before you trust any budget.
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.



