{
    "type": "article",
    "title": "We checked the bill, and our ledger was wrong by half",
    "canonical": "https://www.dealmobile.co.uk/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half",
    "path": "/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half",
    "url": "https://www.dealmobile.co.uk/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half",
    "markdown": "https://www.dealmobile.co.uk/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half.md",
    "json": "https://www.dealmobile.co.uk/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half.json",
    "summary": "Mullet’s ledger said about $38 for a weekend of jobs. The providers billed about $50. How I found the gap, why one OpenRouter model was recorded at about a third of its cost, and the checks that now catch it.",
    "published": "2026-10-25T16:00:00+00:00",
    "modified": "2026-10-25T19:08:26+00:00",
    "image": {
        "url": "https://www.dealmobile.co.uk/sites/default/files/content/images/posts/we-checked-the-bill-and-our-ledger-was-wrong-by-half/hero.png",
        "alt": "A small table headed ledger and providers. Moonshot: $29.94 against $30.06. OpenRouter: $8.04 against $20.15. Below it, requests and tokens match OpenRouter’s dashboard to 0.2%, and a note that the dollars did not."
    },
    "fields": {
        "image_light": null
    },
    "tags": [
        {
            "name": "Python",
            "url": "https://www.dealmobile.co.uk/writing/tag/python",
            "json": "https://www.dealmobile.co.uk/writing/tag/python.json"
        },
        {
            "name": "AI",
            "url": "https://www.dealmobile.co.uk/writing/tag/ai",
            "json": "https://www.dealmobile.co.uk/writing/tag/ai.json"
        },
        {
            "name": "Mullet",
            "url": "https://www.dealmobile.co.uk/writing/tag/mullet",
            "json": "https://www.dealmobile.co.uk/writing/tag/mullet.json"
        }
    ],
    "projects": [
        {
            "name": "Mullet",
            "url": "https://www.dealmobile.co.uk/projects/mullet",
            "json": "https://www.dealmobile.co.uk/projects/mullet.json"
        }
    ],
    "body": {
        "html": "<p>In <a href=\"/writing/the-delegation-ladder\">the delegation ladder</a> I said five jobs passed 16 of 28 steps below Claude for $17.28. That number came from Mullet’s own ledger. Afterwards I checked the ledger against what the providers had charged, and for one provider it was wrong by a long way.</p>\n<h2 id=\"three-figures-for-one-weekend\">Three figures for one weekend</h2>\n<p>They are easy to blur, so here they are apart.</p>\n<ul>\n<li><strong>$17.28</strong> is the ladder report’s total for the five ladder jobs in that post, and nothing else.</li>\n<li><strong>About $38</strong> is what the ledger recorded for everything Mullet ran that weekend.</li>\n<li><strong>About $50</strong> is what the providers billed: $30.06 from Moonshot and $20.15 from OpenRouter.</li>\n</ul>\n<p>The gap between the second and third is almost all OpenRouter.</p>\n<h2 id=\"how-i-found-it\">How I found it</h2>\n<p>I put the providers’ own pages beside the ledger: Moonshot’s console, OpenRouter’s activity page and its key usage endpoint. Moonshot agreed with the ledger to 0.4% ($30.06 billed, $29.94 recorded). OpenRouter’s request and token counts matched the ledger to 0.2%. Its dollars did not: $20.15 billed against $8.04 recorded, and another $2.09 that came from opencode runs outside Mullet’s ledger altogether.</p>\n<p>Counts that match with dollars that don’t means the usage was recorded correctly and the price was wrong.</p>\n<h2 id=\"the-cause\">The cause</h2>\n<p>Mullet gets a model’s price from opencode’s price table. For GLM 5.3 that table gave cache reads almost no price. Cache reads were about 95% of GLM’s tokens, so almost all of its tokens were free as far as the ledger was concerned. By my estimate, read from OpenRouter’s activity chart, GLM’s real cost was roughly three times what I had recorded. I haven’t pulled the exact per-model figure from the activity page, so I won’t print one.</p>\n<p>That changed a conclusion. In the delegation ladder post I had GLM cheaper than Kimi on the same three steps. It wasn’t. It cost more, and it was slower as well. I’ve corrected that post.</p>\n<h2 id=\"what-changed\">What changed</h2>\n<p>Cost no longer comes from a table I trust. Three things replaced it.</p>\n<p><strong>The provider’s own figure.</strong> OpenRouter states the cost of every request, and Mullet asks for it. Other providers are priced from a table you set, with cache reads priced separately:</p>\n<pre><code class=\"language-yaml\">ladder:\n  prices:                     # $ per million tokens\n    moonshotai/kimi-k2.7-code: {input: 0.6, output: 2.5, cache_read: 0.15}\n</code></pre>\n<p>A model with no configured price falls back to $2 in and $8 out per million tokens, high on purpose, so an unpriced model reaches its cap early instead of running up a bill. An attempt’s recorded cost can never fall below what the provider’s responses say.</p>\n<p><strong>Limits that are checked before each attempt.</strong> A daily and a weekly envelope cap everything together, and per-provider limits cap one provider:</p>\n<pre><code class=\"language-yaml\">budget:\n  envelope:\n    daily: 25.0\n    weekly: 100.0\n  per_rung:\n    kimi: {daily: 15.0}\n    openrouter: {weekly: 40.0}\n</code></pre>\n<p>Each attempt on a paid rung gets an allowance from whatever is left under those limits. The key proxy refuses further requests with an HTTP 402 once the attempt has spent it, and the step moves up the ladder. Nothing is limited until you set a value, and <code>mullet doctor</code> warns if you haven’t.</p>\n<p><strong>A check at the end of every job.</strong> The report now has a “Provider check” block: the ledger’s spend for each paid provider beside the provider’s own figure, and whether they agree. Up to 3%, or $0.25 if that is more, counts as agreeing. The provider’s figure includes anything else using the same key, so a difference is a prompt to look, not proof of a fault. This is the one that would have caught the GLM problem on the first job.</p>\n<h2 id=\"the-50-stop\">The $50 stop</h2>\n<p>During that weekend OpenRouter stopped answering at $50. I assumed Mullet’s cap had triggered. It hadn’t. The limit was on the API key itself, set in OpenRouter’s dashboard, and Mullet has no code that sets key limits. A key limit can reset daily, weekly or monthly, or never. Mullet’s own envelope is the control that matters, because it stops a job part way through and says why.</p>\n<h2 id=\"what-i-dont-know-yet\">What I don’t know yet</h2>\n<p>The five-job table in the ladder post should stand. Those jobs ran Kimi and Qwen3 Coder, not GLM, and as far as I can tell OpenRouter’s activity chart agrees with the ledger for them. I haven’t added them up again from the provider’s pages, so read it as a snapshot. I also haven’t benched any other provider that prices cache reads separately. If one does, the table in <code>ladder.prices</code> needs its cache rate before you trust the ledger.</p>\n<p>The code is at <a href=\"https://github.com/tomgould/mullet-public\">https://github.com/tomgould/mullet-public</a>.</p>"
    },
    "markdown_text": "---\ntitle: 'We checked the bill, and our ledger was wrong by half'\ncanonical: 'https://www.dealmobile.co.uk/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half'\nurl: 'https://www.dealmobile.co.uk/writing/we-checked-the-bill-and-our-ledger-was-wrong-by-half'\ntype: Post\npublished: '2026-10-25'\nsummary: 'Mullet’s ledger said about $38 for a weekend of jobs. The providers billed about $50. How I found the gap, why one OpenRouter model was recorded at about a third of its cost, and the checks that now catch it.'\n---\n\n# We checked the bill, and our ledger was wrong by half\n\nPublished 2026-10-25\n\n![A small table headed ledger and providers. Moonshot: $29.94 against $30.06. OpenRouter: $8.04 against $20.15. Below it, requests and tokens match OpenRouter’s dashboard to 0.2%, and a note that the dollars did not.](https://www.dealmobile.co.uk/sites/default/files/styles/almanac_shot_md/public/content/images/posts/we-checked-the-bill-and-our-ledger-was-wrong-by-half/hero.png.webp?itok=Wjc-OQHK)\n\nIn [the delegation ladder](https://www.dealmobile.co.uk/writing/the-delegation-ladder.md) I said five jobs passed 16 of 28 steps below Claude for $17.28. That number came from Mullet’s own ledger. Afterwards I checked the ledger against what the providers had charged, and for one provider it was wrong by a long way.\n\n## Three figures for one weekend\n\nThey are easy to blur, so here they are apart.\n\n- **$17.28** is the ladder report’s total for the five ladder jobs in that post, and nothing else.\n- **About $38** is what the ledger recorded for everything Mullet ran that weekend.\n- **About $50** is what the providers billed: $30.06 from Moonshot and $20.15 from OpenRouter.\n\nThe gap between the second and third is almost all OpenRouter.\n\n## How I found it\n\nI put the providers’ own pages beside the ledger: Moonshot’s console, OpenRouter’s activity page and its key usage endpoint. Moonshot agreed with the ledger to 0.4% ($30.06 billed, $29.94 recorded). OpenRouter’s request and token counts matched the ledger to 0.2%. Its dollars did not: $20.15 billed against $8.04 recorded, and another $2.09 that came from opencode runs outside Mullet’s ledger altogether.\n\nCounts that match with dollars that don’t means the usage was recorded correctly and the price was wrong.\n\n## The cause\n\nMullet gets a model’s price from opencode’s price table. For GLM 5.3 that table gave cache reads almost no price. Cache reads were about 95% of GLM’s tokens, so almost all of its tokens were free as far as the ledger was concerned. By my estimate, read from OpenRouter’s activity chart, GLM’s real cost was roughly three times what I had recorded. I haven’t pulled the exact per-model figure from the activity page, so I won’t print one.\n\nThat changed a conclusion. In the delegation ladder post I had GLM cheaper than Kimi on the same three steps. It wasn’t. It cost more, and it was slower as well. I’ve corrected that post.\n\n## What changed\n\nCost no longer comes from a table I trust. Three things replaced it.\n\n**The provider’s own figure.** OpenRouter states the cost of every request, and Mullet asks for it. Other providers are priced from a table you set, with cache reads priced separately:\n\n```yaml\nladder:\n  prices:                     # $ per million tokens\n    moonshotai/kimi-k2.7-code: {input: 0.6, output: 2.5, cache_read: 0.15}\n\n```\n\nA model with no configured price falls back to $2 in and $8 out per million tokens, high on purpose, so an unpriced model reaches its cap early instead of running up a bill. An attempt’s recorded cost can never fall below what the provider’s responses say.\n\n**Limits that are checked before each attempt.** A daily and a weekly envelope cap everything together, and per-provider limits cap one provider:\n\n```yaml\nbudget:\n  envelope:\n    daily: 25.0\n    weekly: 100.0\n  per_rung:\n    kimi: {daily: 15.0}\n    openrouter: {weekly: 40.0}\n\n```\n\nEach attempt on a paid rung gets an allowance from whatever is left under those limits. The key proxy refuses further requests with an HTTP 402 once the attempt has spent it, and the step moves up the ladder. Nothing is limited until you set a value, and `mullet doctor` warns if you haven’t.\n\n**A check at the end of every job.** The report now has a “Provider check” block: the ledger’s spend for each paid provider beside the provider’s own figure, and whether they agree. Up to 3%, or $0.25 if that is more, counts as agreeing. The provider’s figure includes anything else using the same key, so a difference is a prompt to look, not proof of a fault. This is the one that would have caught the GLM problem on the first job.\n\n## The $50 stop\n\nDuring that weekend OpenRouter stopped answering at $50. I assumed Mullet’s cap had triggered. It hadn’t. The limit was on the API key itself, set in OpenRouter’s dashboard, and Mullet has no code that sets key limits. A key limit can reset daily, weekly or monthly, or never. Mullet’s own envelope is the control that matters, because it stops a job part way through and says why.\n\n## What I don’t know yet\n\nThe five-job table in the ladder post should stand. Those jobs ran Kimi and Qwen3 Coder, not GLM, and as far as I can tell OpenRouter’s activity chart agrees with the ledger for them. I haven’t added them up again from the provider’s pages, so read it as a snapshot. I also haven’t benched any other provider that prices cache reads separately. If one does, the table in `ladder.prices` needs its cache rate before you trust the ledger.\n\nThe code is at <https://github.com/tomgould/mullet-public>.\n\n- **Projects:** [Mullet](https://www.dealmobile.co.uk/projects/mullet.md)\n- **Tags:** [Python](https://www.dealmobile.co.uk/writing/tag/python.md), [AI](https://www.dealmobile.co.uk/writing/tag/ai.md), [Mullet](https://www.dealmobile.co.uk/writing/tag/mullet.md)\n\n---\n\nIndex of this site: [llms.txt](https://www.dealmobile.co.uk/llms.txt)\n"
}