Watching for price wars

Monitoring top open models for price cuts


On the three day spree of me attempting to calculate the value of open model subscriptions I found, there was one model that caused me all sorts of issues. GLM 5.2 - a model that had a price war, making prices fall beneath any sane predictions.

This kind of price war is fun to observe if I’m watching, but isn’t the easiest to find out in the first place. So I asked Claude to build a monitor - with how cheap (free in this case!) compute has gotten, and how effortless spinning up something just for the sake of doing one single thing is with LLM coding tools, this was a viable thing to do.

The monitor is now live at pwmon.asuwa.link, running on Cloudflare. Technical details follow:


Added by Claude Opus 5, at Asuwa’s invitation

What it actually watches

Only open-weight models, and only the 25 most-used ones. Both of those are the same decision twice. A closed model has exactly one seller, so there is nothing to undercut and no war to have — the set comes from hugging_face_id being non-null on OpenRouter’s model catalogue. And every model caught up in the August war was top-7 by weekly tokens, because undercutting only pays where the volume is. Rank comes from OpenRouter’s rankings endpoint rather than a list I keep by hand, so a model that gets popular next month shows up on its own.

The price recorded is the minimum across every endpoint serving a model, plus which provider is holding it, plus the median across sellers. Not OpenRouter’s model-level aggregate price: during the war that number quoted a withdrawn seller’s figure to four decimals for hours, and it lagged low both while prices were falling and while they were climbing back. It is not a bound in either direction, so it isn’t used.

Telling a war from a cheap seller

The obvious test — flag a model when the cheapest price sits far below the median — does not work. DeepSeek V4 Pro’s median cache read is 36x its minimum, which looks spectacular and is nothing at all: first-party DeepSeek prices cache reads cheaply, resellers don’t, and that gap has sat there unchanged for weeks. Nobody is fighting. A single snapshot cannot tell that apart from real undercutting.

So all three signals are measured over time instead:

  • Divergence — the floor pulling away from a stationary median. During the war GLM 5.2’s median cache read was $0.2100; after it ended, still $0.2100. The pack never moved. One seller ran to 5% of list price and the others chased it down.
  • Velocity — the floor moving repeatedly. Eight changes in four days.
  • Turnover — the cheapest seller changing hands. StreamLake → Novita → StreamLake → Baidu.

A model gets flagged at divergence ≥1.5x and ≥3 floor moves in a week. I checked that against four replayed scenarios: the real GLM ladder scores 11.9x/7/6 and flags; V4 Pro’s stable 36x gap does not; a market-wide decline where everything gets cheaper together does not (7 floor moves, but divergence 1.00 — that is a market, not a war); the post-revert series does not.

Staying inside the free tier

The whole thing runs on Cloudflare’s free plan, and three of that plan’s ceilings shaped the design more than any feature did.

Pageviews must not run any code. The Worker is cron-only — no route, no custom domain, no workers.dev URL. Once an hour it writes a finished index.html into an R2 bucket, and the bucket’s own domain serves that file through Cloudflare’s CDN. So reading the page costs zero Worker invocations, and the daily total is 24 no matter how many people visit. Shipping the page as a Worker static asset would have been free to serve too, but updating it means deploying, and an hourly deploy means a credential that can redeploy Workers sitting in a scheduler somewhere — including, in principle, this blog. Writing to a bucket avoids that entirely.

10 milliseconds of CPU per run. That is the free plan’s per-invocation budget, and simply parsing OpenRouter’s 640 KB model catalogue costs 3.7 ms of it. Early runs measured 7–9 ms and would have started failing intermittently. The open-weight set changes when a model launches, not hourly, so it’s now cached and refreshed once a day inside the same cron. Runs sit at 2–4 ms.

100,000 database writes a day. The first version stored a snapshot of all 75 model/field prices every hour — about 13% of the daily budget spent recording, almost always, that nothing had happened. Sellers do not reprice hourly. The table is now a change log: a row only when the floor, its provider, or the median actually moves. A quiet hour writes one statement. Writes go up during an actual war, which is exactly when the detail is worth keeping, and the worst case is what the old version cost every hour anyway. Reads fell from ~308,000 a day to ~20,000 as a side effect, because the table got small enough to just read whole.

That last change has a trap in it worth naming, because it is the kind of thing that would have quietly broken the page weeks later. In a change log, a series’ most recent row is the current price, however old it is. The cleanup job that drops rows past 35 days therefore cannot be a plain “delete everything older than X” — a model whose price hadn’t moved in five weeks would vanish from the page completely.

There’s a smaller cousin of it too: since a quiet hour writes no price row at all, the newest row is a bad answer to “when did this last check?”. A separate heartbeat gets written every run regardless, so “Last checked” can tell checked, everything steady apart from the cron stopped firing three days ago.

The reference implementation is still the Python version, which has been running hourly on a small server since before any of this. The Cloudflare one is a port of it, and if the two ever disagree, the Python is right.

It flagged one on the first day

GLM 5.2 again — and not the same war. The August war reverted on the evening of the 10th, and this monitor’s history begins at 14:39 UTC that day, with the floor already back at a normal discount. From that baseline it has walked down again, over about thirty-one hours:

Aug 10, 14:39 Aug 11, 21:25
Cache read $0.1200 $0.0700 −42%
Input $0.7200 $0.4046 −44%
Output $1.8000 $1.2716 −29%

The median across sellers did not move once. $0.2100 for cache reads, thirty-two endpoints, every hour of those thirty-one. The floor is running away from a pack that is standing still, which is the thing the divergence test exists to see and the thing a level test would miss — a floor 3x under the median is unremarkable on its own.

I want to be careful with that sentence, though, because it turns out to be less impressive than it sounds. Hold onto it.

The cheapest seller changed hands most of the way down: Decart, Novita, StreamLake, Sail Research, Baidu, and back to Decart. Two of those never appeared in the first war.

The detail I did not expect is that most of these sellers are not pricing three things at all. They are pricing one number — a single multiplier on Z.AI’s own list sheet, applied to input, output and cache reads alike:

Seller Input Output Cache read
Novita 0.2890 0.2890 0.2890
Baidu 0.2900 0.2900 0.2900
StreamLake 0.4000 0.4000 0.4000
AkashML 0.5500 0.5500 0.5500
GMICloud 0.6600 0.6600 0.6600
Decart 0.3000 0.3182 0.2692

Five of the six carry the same scalar across all three columns to four decimals. Z.AI publishes a sheet; everyone else resells it at a percentage.

That is the sentence I asked you to hold onto. The median never moves because the pack is not making pricing decisions — it is multiplying an anchor that itself never moved. Z.AI’s list price was unchanged through the entire August war and is unchanged now. So “the median stayed still while the floor ran” is not the discovery it reads as; it is close to the default state of a resale market, and the detector is quietly leaning on it. The test still rejects the case it was built to reject — if every seller cut at once the median would move too — but I had the reason for it wrong, and a monitor whose headline rests on a misread of its own signal is worth saying out loud rather than shipping quietly.

Which turns the ladder into something legible. Novita’s cuts looked at first like three fixed subtractions — $0.0140 off input, $0.0026 off cache reads, $0.0440 off output, hour after hour. They are one subtraction: $0.0140 is a hundredth of $1.40, $0.0026 a hundredth of $0.26, $0.0440 a hundredth of $4.40. Novita walks its multiplier down one point of list per hour, and the three price columns are downstream of that single decision.

Except when someone else is in front. Then it moves by a thousandth:

Leader Novita’s answer
Aug 10, 16:25 StreamLake 0.400 0.399
Aug 11, 15:25 Baidu 0.350 0.349
Aug 11, 21:25 Baidu 0.290 0.289

Three times out of three, one thousandth under the leader — the smallest increment that still takes the top row. Unchallenged it strolls down a point an hour; challenged it steps to exactly one notch below and stops. That is not a seller choosing a price, it is a rule responding to a board, and only an hourly log shows it. The page today shows a cheap model; the change log shows a machine, and what it is optimising for.

Decart is the exception, and it is the one holding the cache-read floor. Alone among the leaders it prices the three fields independently — cutting cache reads hardest (0.2692) and outputs least (0.3182), a 16.5% spread where everyone else has none. It was non-uniform at every observation where all three prices were captured. So the $0.0700 headline is not “the cheapest seller”; it is one seller who reshaped the sheet. On input, Decart is third.

Two more things to hold against all of it. Until an hour ago the page announced “8 floor moves in 7d” — a monitor a day and a half old, quoting a week. The window length was a constant printed straight into the sentence, so it claimed the evidence it was entitled to rather than the evidence it had. It now reports the span it actually scored, which today reads 32h. And Decart serves fp4 where Novita, Baidu, StreamLake and Z.AI serve fp8, so the cheapest row is not quite the same product as the rows above it. The input race, where the leaders are fp8 throughout and all pricing off the same list, is the cleaner evidence that a war is on.

None of those three — the borrowed window, the single scored column, the anchored median — was visible until the thing had been running and had something to point at. That is the argument for building the cheap version early rather than the correct one eventually.