The cost came from the wrong ledger

8–12 minutes
measured on my own machine
The cost came from the wrong ledger

My content chain built a blog post on its own for the first time. At the end it handed back one line for the dashboard card, and the cost in that line said 0. The first post had cost $3,00 across five runs. One of them got through, for $0,80. The card would have said free. Nothing crashed. No check went red. It was a silent 0 in a card that had to say $3,00.

What I wanted was plain. I wanted to know what a run costs, on the card, not in a log. The chain has a cost cap per run and a cap on how many finished posts may wait at once. A cap you cannot see the spending against is a guess with a number on it. And the post this chain had just built was itself about cost: #515, “147 runs, 100.13 dollars, zero posts”.

I believed a 0 meant it had cost nothing. I saw that 0 twice and took it for a good result both times. It looked plausible enough. A low number did not surprise me, so I never opened the ledger to compare. That was the whole mistake. I read the value and never asked where it came from.

The 0 meant that nobody had looked. The audit module keeps two kinds of files. readAuditRun() reads audit/<datum>/<runId>.ndjson, one file per run. The costs live somewhere else, in audit/cost/<datum>.ndjson: one file per day, shared by all runs. The driver asked the per-run book for money that is only ever written into the daily book. It found nothing and reported 0. A missing number looks exactly like a low one, and nothing downstream of that line can tell the two apart.

Two silent zeros in one morning

It was not the first silent zero that day. The first run without hands, earlier that morning, had found another one. The image step computed the media ID of the featured image and printed it only into a command line meant for a person to type. The automated path types nothing. So #515 reached the site with featured_media: 0, and the publishing gate rejected it: “The post has no featured image.” The chain had produced something its own exit never accepts.

f14db9b7 · 2026-09-12

Die Kette bekommt einen Treiber — und die Media-ID des Titelbilds hört auf, nur eine Konsolenzeile zu sein

The chain gets a driver, and the media ID of the featured image stops being just a console line

The same commit records why the expensive step runs on opus. I measured both models on the same fact sheet. Sonnet failed both attempts for $0,63 and invented $100 where 100,13 is on record. Opus held on the first attempt for $0,80. That comparison is the reason the cost card matters at all. Choosing a model is choosing a price, and a card that says 0 for every run hides the one thing the choice depends on.

372d4660 · 2026-09-12

Die Kosten kamen aus dem falschen Buch — eine stille 0 in einer Karte, die $3,00 heißen musste

The costs came from the wrong ledger: a silent 0 in a card that had to read $3,00

The fix moved the lookup to where the ledger lives. getRunCost() now sits in audit.mjs, sums all attempts of a run, and returns null for a run it does not know. Explicitly not 0. That difference is the point of the commit. A 0 is a statement about money. A null is a statement about the ledger. A card that shows null says we do not know, which is true, instead of free, which was false.

The same commit gave the driver a flag that reads the state of a finished post from the files without running a single step. It is not a test door. It is the restart for the case where the chain runs through and only the reporting fails. Without it, the only way to fix the card would be to write the same article again and pay for it again. One dead field went too: the category was always null, because it only comes into existence later, as a constant in the posting step.

2378ed8f · 2026-09-12

Der Banddeckel ist anhebbar wie der Kostendeckel daneben — Vorgabe bleibt 2

The band cap can be raised like the cost cap next to it; the default stays 2

The third commit came from wanting to see the chain run for real. The band cap lets 2 finished posts wait at once, my default from 08.09. Two were already waiting out their veto window, so the next real run would have happened on 14.09. An untested write path is exactly the one that dies at 06:40, and the commit notes that had already happened twice that day. An environment variable now raises the cap for one run, built the same way as the cost cap, and every run prints the value it works under.

The three commit subjects of that morning, in the original.
The three commit subjects of that morning, in the original.
  1. 2026-09-12Built a driver that runs the chain’s five steps as separate processes, picks the next fact sheet on its own, oldest first, and hands back one machine-readable line. The cost it reported was 0. I saw that 0 twice and took it for a good result.
  2. 2026-09-12Found that the driver asked the per-run audit file for costs that are only written into the daily cost file. Moved the lookup next to the ledger, summed every attempt, and made an unknown run return null instead of 0.
  3. 2026-09-12Made the band cap liftable for a single run, so the chain could run live while two posts waited out their veto. Tested that a typo in the variable does not quietly set the cap to 0.
  4. 2026-09-12Wrote the handover for the image chain: nine errors, all found by runs on real data, by looking at an image, or by measuring in production. No test caught any of them.
  5. 2026-09-12Wrote the handover for the build loop: four locks fixed at their cause, and #515 built and posted, announced for 14.09.
All 8 steps of the record in the order they happened. The post retells five of them.
All 8 steps of the record in the order they happened. The post retells five of them.

The record does not name a tool I ran. It names files. Two test files changed, bandlage.test.mjs and bildkosten.test.mjs, so node –test is inferred from the kind of file, not quoted from me. The cap commit says the dangerous direction is covered: a typo would otherwise look like the band standing still, while in truth the variable was nonsense and nothing would ever be built again. The record does not say which tests ran or how long any of this took.

What the record supports, and what it leaves open.
What the record supports, and what it leaves open.

The costs now come from the right ledger, and the band cap can be raised like the cost cap next to it. The default stays 2. That part is solved. What is not shown: the record has no card displaying a real cost after the fix, and the first post of this chain was still waiting for 14.09. The handover names an open end of the same class, one level up. The drafting step skips a post that already exists, so a corrected article never reaches the site. That one is not built, because it needs a decision first.

Three commits, all on 2026-09-12: f14db9b7, 372d4660, 2378ed8f, plus the handovers written the same day. Fact sheet measured 2026-09-17.

f14db9b7 · 372d4660 · 2378ed8f

Georg Singer

Georg Singer

I automate my own projects in public and write down what each one cost. Physics degree, doctorate in computer science, nine systems in production on hardware I own, all of it built in my own time.

More from the log