← The Journey

Twenty-Three Cents to Read a Photo, and Almost None of It Was the Photo

Projects 2026-07-25 · Saturday · 7:55 PM 5 min read 92% AI Eric Li

A short day, two bursts with a long gap in the middle. The first was tidying up after yesterday's git server build. The second was meant to be a quick question and turned into the most useful thing I've found in a while: one of my pipelines was charging me a quarter every time it looked at a photograph, and 97% of that was packaging.

What does one call actually cost?

I have a parked side project that watches a folder for photos of handwritten work orders, transcribes them into a CSV, and files the originals. It works, it's been disabled for a while, and before turning it back on I wanted to know what it costs to run. So I asked the plain version: how many tokens does one image cost?

I couldn't answer it, which was the first finding. The extraction step shelled out to claude -p, the Claude Code CLI in one-shot mode, and read exactly one field out of the JSON that came back: the result. The CLI had been handing me usage and total_cost_usd on every call and I was throwing both in the bin, so the pipeline log had zero cost lines in it.

So I measured instead of estimating. Three real runs against the same sample photo, a 1248×1760 shot of a work order, all three returning identical transcriptions at 0.85 to 1.0 confidence. The costs came back $0.2499, $0.2189 and $0.2277. Call it 23 cents a photo.

The bill was almost entirely envelope

The interesting part isn't the number, it's the breakdown. The actual payload per photo is small: a 1,196-character prompt is about 300 tokens, and the image itself is about 2,900. So roughly 3,200 tokens of real work.

I was being billed for about 117,000 input tokens.

The other 114,000 were the agent harness: system prompt, tool definitions, session scaffolding, shipped up the wire on every photo, and twice per photo at that, because one turn goes on deciding to read the file and a second on reading it and answering.

Then the part that made me laugh. Nearly all of the cost was the prompt cache, specifically the cache write: 36,312 tokens at the 2× write multiplier, $0.218, which is 87% of the whole bill. Caching is meant to be the cheap path — it pays for itself the second time you send the same prefix, and reads come back at a tenth of the price. But every claude -p invocation is a fresh session with a different prefix, so the cache never warmed. Three back-to-back identical runs and the write count sat at 31–36k every time. I was paying a premium to build a cache and then throwing it away before anything could read it. Fresh input tokens on that bill: four.

Second finding, less funny: a blurry photo that fails validation costs exactly the same as a good one. It burns the full call and then lands in the failed folder.

Ten minutes, seventeen tests

The go-ahead was nine words, so the rewrite started immediately. It's a small port: drop the subprocess, call the Messages API directly with the image as a base64 content block, and let structured outputs guarantee the JSON shape instead of asking for it politely in prose. That last change deleted a function whose whole job was stripping markdown code fences off the model's reply, and with it the "did not return valid JSON" failure mode. Ten minutes, and 17 of 17 tests stayed green, which is only true because past-me mocked the OCR call behind an injection point instead of letting it reach out from inside the test suite.

Two reversible judgement calls came with it: thinking off, since straight transcription doesn't need it and it's pure cost, and images downscaled to 2000px on the long edge, since a 4032×3024 phone photo is around 16k tokens against 4k for the same shot at 2000px. The cost log line goes before validation, so failed photos still get counted. They still cost money.

Here's the honest bit. The estimated cost on the new path is about 1.7 cents a photo, roughly 14× cheaper, which at fifty photos a week is $11.50 against $0.85. That number has never been measured. There's still no API key on the box, so not one real call has gone through the new code. I've verified the token counts, the four image formats, the tests. I have not verified a bill. And there's a decent chance the $0.23 wasn't metered money at all but plan usage, which would make the comparison shakier still. It's an estimate wearing a very convincing suit.

Deciding not to keep something

The earlier burst was Forgejo housekeeping, mostly carried over from yesterday: extending a fine-grained GitHub token so the push-mirrors can write, plus the useful detail that editing one doesn't change the token string, so nothing on the server needs touching afterwards.

The one real decision took a sentence. The old test-website repo is obsolete, replaced by the current portfolio, and shouldn't be pushed anywhere at all. So it got archived read-only with a description saying why, and that one call quietly deleted a planned push-mirror, an origin flip, and a whole repo from the setup instructions. Four documents shrank. The pending work went from three repos to two. Deciding not to keep something is work, and it's the cheapest kind there is.

Meanwhile, at 4am

While none of that was happening, the nightly update pipeline did its rounds unattended. It found Home Assistant and n8n stale and asked rather than deciding, noting that both publish only digest hashes rather than version tags so it couldn't read release notes first. Approved, applied at 04:55:50. Two minutes later the stack documentation regenerated itself off the container-create events, catching both containers at "Up 50 seconds" on the way past.

Two hours of actual work, and the cheapest thing I did all day was read the receipt.