◆  CRAFT  ·  FIELD NOTES

A $0.69 Production Fix with OpenCode Zen

A transcript-style account of closing a live newsletter paywall leak on the Cyber Toolchain site, end to end, for less than a dollar — with DeepSeek v4 Flash as the only model in the room.

The premise

I've been running all my coding sessions through OpenCode Zen — the CLI harness that ties together my provider, my project instructions, and a persistent store of every session's cost and token usage — and I've been deliberately using DeepSeek v4 Flash (opencode/deepseek-v4-flash) as the workhorse model. Flash-class models are the interesting part of the LLM price curve: cheap enough that a whole real job costs cents, capable enough that — on well-scoped work — the quality gap vs. a flagship model barely shows.

This is the full narrative of one such job: a production paywall fix on a live site, from first commit to live verification to cleanup. It is a real session, the numbers are the numbers, and the entire thing cost $0.69.

The bug

The site is the Cyber Toolchain newsletter. It has a paid series called Diff — eight issues (docs-001docs-008), of which 5–8 were already locked behind unguessable URL keys. Issues 1–4 were gated by this line in the site's routing logic:

DIFF_FREE_THROUGH = 4

Meaning: any issue with number ≤ 4 was served publicly by its number/newsletter/diff/1, /2, /3, /4. The intent had been "the first four are free." The problem: only issue 4 was ever meant to be the free sample. Issues 1–3 were subscription content, and they were sitting on the public internet, readable by anyone who typed the obvious URL.

The task, as handed to me: keep only Diff issue 4 readable by number. Issues 1–3 become subscription-only, locked the same way as 5–8.

Doing it in a worktree

The very first thing, before any edit: create a fresh git worktree.

This project is worked on by several AI sessions at once. The main checkout (~/repos/cybertoolchain/generator) is where they all collide — at the moment of this job it was carrying ~100 files of other sessions' uncommitted work. So the rule, written into the project's own instructions after past incidents, is: branch from main, work in a worktree named after the work.

git worktree add -b worktree-diffgate ../generator-diffgate main

Working tree: generator-diffgate, branch worktree-diffgate. Every edit in the rest of this story happened there, isolated from the shared checkout.

One dead end worth confessing: my first worktree was diff-newsletter, created in the built-output repo — the deploy-only site repo — before I'd oriented myself. It was the wrong repo for this work and I removed it at the end. Wrong turns are cheaper to admit than to defend.

The change

The core edit, in site/src/lib/series.ts:

- DIFF_FREE_THROUGH = 4
+ DIFF_FREE_ISSUES = new Set([4])

with isIssuePublic now consulting the set — so diff/1, diff/2, diff/3 no longer build as public numeric pages, and diff/4 stays public as the sample.

Three things needed to mirror that decision, and each is a classic "the fix lives in more than one place" trap:

  1. tct/src/send.py — the email sender decides which URLs subscribers get. Mirror: _DIFF_FREE_ISSUES = {4}.
  2. tct/scripts/backfill_diff_url_keys.py — the script that mints unguessable keys for locked issues. Mirror: FREE_ISSUES = {4}.
  3. The keys themselves — minted for 1–3: - docs-001PHFRCSh2TkceAn4JWO9n7A - docs-002Z2xg2g0QdPh9sIRC97EBhg - docs-0030bmlp2c3y5oNIp21Oy0kdQ

A subtlety in the RSS feed: since the numeric pages for 1–3 no longer build, the feed can't link to them. Instead it points each paid issue at the site's public sample page — /samples#diff-001 — keeping the issue number in the URL fragment so each feed item keeps its own GUID.

Verification before deploy

Cheap models get a reputation for guessing. The cure is mechanical verification:

  • 1385 site tests pass (series.test.ts, loadIssue.test.ts, feed.test.ts all updated to assert the new gate).
  • tct send/delivery/billing subset: 209 passed — the email-sender mirror was tested, not just edited.
  • Build inspection: numeric diff/1|2|3 pages absent from the output; keyed pages + diff/4 present; the sitemap lists only diff/4; keyed pages carry noindex; no leaks into entries.json, the archive, or the feed.

That last set is the part that matters: a paywall fix isn't verified by green tests, it's verified by what the built site exposes.

Deploy and live check

Deploy ran from the worktree, out to GitHub Pages. Then the verification that counts — hitting the real routes:

  • /newsletter/diff/1, /2, /3 (numeric) → 404
  • the three keyed URLs → 200
  • /newsletter/diff/4200
  • sitemap → lists only diff/4

One operational detail: the live checks showed the old behavior until the CDN edge cache refreshed, then flipped to the new behavior. Verify a cache-invalidating deploy twice — once naively, once after the edge settles.

The project normally requires a changelog entry before every deploy. I deployed without one, with the user's explicit sign-off, because this was a security fix that should not wait on prose. Rituals can be waived by the human; they can't be waived by the tool on its own.

The merge home: surviving a shared checkout

Back in main. The branch was a fast-forward (one commit, 3ecc67e9, "Keep only Diff issue 4 readable by number"). But here's where the worktree discipline pays off and the real skill shows.

The shared main checkout was dirty with ~100 files of other sessions' uncommitted work. Of the 14 files in my commit, exactly two overlapped with that in-flight work: TODO.md and site/src/lib/feed.test.ts. The project's own rules are explicit about this hazard — past incidents of git stash (whole-tree) wrecking other sessions' work are documented as scars — and the sanctioned technique is a path-limited stash:

git stash push -- TODO.md site/src/lib/feed.test.ts

Park just the two overlapping files, fast-forward the merge, then restore:

git stash pop

Exactly one conflict, in feed.test.ts, and it was a happy conflict — proof the merge was doing its job. The peer's in-flight change had rewritten the test domain from cybertoolchain.github.io to cybertoolchain.io; my change had rewritten the diff-link assertions to #diff-001. The resolution combined both intents:

expect(links).toContain('https://cybertoolchain.io/samples#diff-001');

Then: unstage both files back to their original unstaged state (the peer's work was never committed; it must not silently become part of a future commit), drop the stash, and re-run the 21 feed tests — green.

Cleanup

Remove the worktree, delete the branch, and — from the earlier dead end — remove the diff-newsletter worktree too:

git worktree remove --force ../generator-diffgate
git branch -D worktree-diffgate

The repo ends where it started, minus one shipped fix.

The bill

From opencode's own session ledger (opencode.db):

Model DeepSeek v4 Flash (variant: high)
Input tokens 323,573
Output tokens 100,444
Cache reads 21,858,560 (~21.9M)
Recorded cost $0.69

Two observations worth more than the total:

1. Cache reads dominate. Output — the expensive thing people worry about — was 100K tokens. Cache reads were 21.9M: every turn re-sends the cached prompt, and long sessions with big context (project instructions, large files) pay for that over and over. This is the lever on Flash-class cost, and it's exactly why the project compresses its CLAUDE.md files religiously. A session's price is set mostly by its context diet, not its model.

2. The number is an estimate. The $0.69 is what opencode computed using its built-in price table. The true invoice comes from the provider (Zen). Close, but reconcile against the real dashboard before quoting a number in public.

What a cheap model is (and isn't) for

The useful finding of this session isn't "DeepSeek v4 Flash is great." It's: the job was 90% process, and process is model-independent. Worktree discipline, mirror-aware edits, mechanical test verification, live-route checking, path-limited stashes, careful conflict resolution. None of that is a harder model doing heavy lifting; all of it is the human and the harness keeping the loop tight.

The model-specific parts — reading the series.ts gate and its three mirrors, drafting the test assertions, resolving the one-line conflict — are precisely the kind of well-scoped, judgment-lite work where a Flash-class model is indistinguishable from a flagship. Paying ten times as much for a bigger model here would have bought almost nothing measurable.

Lessons

  1. Match the model to the job, and the job was mostly process. The expensive tokens aren't the model's output; they're the repeated re-reads of your context. A lean CLAUDE.md and short sessions are worth more than any model upgrade.
  2. Work in a worktree, always — and in a shared checkout, reach for path-limited stashes, never git stash of everything. The latter has destroyed other sessions' work here before; it's written into the project rules as a scar.
  3. Mirrors are where paywall bugs hide. One decision (only issue 4 is public) had to land in four places: the routing gate, the email sender, the key-minter, and the minted keys themselves. Edit one and the fix silently leaks.
  4. Verify the built site, not just the tests. Green tests prove logic; the sitemap, the 404s, the noindex tags, and the feed prove exposure. For a security fix, exposure is the metric.
  5. Cache invalidates; check the live site twice. The CDN served the old behavior until it refreshed. A first check that "still shows the bug" isn't a failed deploy.
  6. Kill dead ends early. The diff-newsletter worktree in the wrong repo cost nothing but time; leaving it would have cost confusion forever.
  7. A real job, end to end, for 69 cents. Tests, deploy, live verification, merge, conflict resolution, cleanup — a complete production change on a cheap model, where the price ceiling was set by context discipline, not capability.

Generated from a live OpenCode Zen session on 2026-08-29, working in ~/repos/cybertoolchain/, model opencode/deepseek-v4-flash, session ses_fb0a9fd72ffe8cYdVuzfl2aH9k. Cost figure from opencode's session ledger; actual invoice is on the provider side.