×px: a coding agent built around DeepSeek’s prefix cache

px: a coding agent built around DeepSeek’s prefix cache

A 30-turn session costs $0.0149 off-peak. Most of the saving comes from never changing a byte the cache has already seen.

Most coding-agent harnesses treat the model provider as a text endpoint you can swap. DeepSeek isn’t one. Its prefix cache is automatic and byte-exact, and a hit costs 2% of a miss. Thinking mode bills its reasoning tokens at the output rate. The API aliases model ids without telling you. None of that shows through a provider abstraction, and all of it decides what a session costs and whether it works.

px is the harness I wrote with those behaviours as the design input instead of a compatibility problem. It’s TypeScript on Bun. A 30-turn session costs $0.0149 off-peak.

The request body is a string

An agent re-sends its whole conversation every turn, so cached input is the biggest line in the bill. On DeepSeek’s flash model a cache hit is $0.003 per million tokens and a miss is $0.15. The cache matches exact bytes in 128-token steps. Change one byte anywhere in the prefix and everything after it is a miss.

JSON.stringify over a request object leaves key order to whoever last added a field, so px never builds the body that way. It concatenates bytes, in three zones:

Zone Holds Rule
A effort marker, system prompt, tools array frozen and hashed at boot; nothing volatile
B the conversation append-only; never re-serialized
C cwd, branch, clock, budget, todos, verify status rebuilt every turn; always last

Zone A gets a SHA-256 at boot and is checked on every send, because the API never reports the drift that breaks it. Two cases from development: appending a tool at the end of the array instead of in sorted position dropped the cache hit from 20,224 tokens to 3,328, and two identical tool arrays with different key order dropped it to 3,200. prompt_tokens was the same in both. So px sorts object keys and tools by name, and emits no whitespace.

Zone B keeps each message as the exact bytes that will be sent for the rest of the session, with no accessor that returns a parsed object. Tool-call arguments are replayed as the raw string the stream produced. Parsing and re-stringifying them would normalise whitespace, which is a cache break with no symptom.

Zone C takes everything that changes. Leaking one Zone C value into Zone A took the session hit rate from 86.0% to 25.3%, 4.8 times the input cost, and the gap widens as the conversation grows. Zone C is free in return: the last 128 to 255 tokens fall outside the cached region anyway.

Two more checks. Each turn compares the miss tokens with the size of the tail just appended; a much bigger miss means something above Zone C changed, and px records it. And at boot px sends two identical minimal requests. If the second doesn’t hit the cache, px stops rather than run for an hour at 50 times the price. The canary costs about $0.0001.

Reasoning effort is a cache namespace

The thinking marker sits at byte 0 of the prefix, and the cache isn’t shared across settings. Changing reasoning effort mid-session throws the whole prefix away. On a 200K-token context that’s $0.132 of re-prefill on the pro model before a token comes back, about nine times the whole reference session.

So a session pins one lane. About 97% of calls run on flash with thinking off, which also:

  • removes a hidden preamble of 26 tokens on flash and 79 on pro from every request
  • bills no reasoning tokens
  • makes a forced tool_choice legal; with thinking on it’s a 400
  • makes temperature: 0 work, so tool arguments come back byte-identical and the wire format can have golden tests

When the loop needs more depth it calls consult(), which opens a separate, short conversation on a reasoning lane with only the question and the evidence it was handed. The answer comes back as a tool result in the append-only tail, so the main prefix never moves and the expensive lane is billed on a few thousand tokens instead of the whole conversation. The pro model’s lane is pinned to low effort: at medium and above it ran 84 to 315 seconds a call and produced 2.5 to 3 times less than flash with thinking off.

The machine owns the verdict

A coding agent that decides for itself whether it succeeded will report success, because that’s the cheapest thing to say. px doesn’t ask.

  • Every px run works in its own git worktree on its own branch. Paths are resolved through symlinks before the containment check.
  • The verify command runs once on the untouched worktree before the first edit, and the failing tests are recorded.
  • passed needs the command to exit 0 and no test that passed at baseline to fail now. A change that turns one test green and two others red can exit 0 on a narrow command; the failing-set delta catches it.
  • When the model calls its finish tool, px runs verify. If the delta isn’t clean, px replaces the finish tool’s result in place with the failing output. The conversation stays append-only, the cache stays warm, and from the model’s side its attempt to stop returned “not yet, here’s what broke”.
  • A repo that was red and is still red after three attempts reports partial, with how many baseline failures went away. Only passed exits 0.

px run won’t start without a verify command. It reads .px/verify.toml or infers one from the tree, and if neither works it exits before the first API call. A guessed command that exits 0 for the wrong reason is worse than none.

What it costs

The reference session: 30 turns, context growing from about 6K to 30K tokens, 86% cache hits, thinking off. Per turn, at 18,000 mean prompt tokens and 120 output tokens:

Tokens Rate per million Cost
Cache hit 15,480 $0.003 $0.0000464
Cache miss 2,520 $0.15 $0.0003780
Output 120 $0.60 $0.0000720
Per turn $0.000496

Thirty turns is $0.0149. DeepSeek charges double on weekdays from 01:00 to 04:00 and 06:00 to 10:00 UTC, so the same session in those hours is $0.0298. px shows the model the current multiplier every turn.

On the same token counts, an untuned harness on the same model, with the cache collapsed and thinking left on, lands near $0.075. Sonnet-class list prices with a perfectly warm cache come to about $0.48. That last figure is list-price arithmetic, not a measurement, and real sessions with pauses in them would land worse. Every call px makes goes into a local SQLite ledger with its hit, miss and output split. The balance on a dedicated API key is the audit.

What it doesn’t claim

The containment is the worktree, not a prompt per action. Prompts train you to click yes. Inside the worktree ordinary edits need no approval, and the worst case is one git worktree remove --force. px refuses the commands that escape it: sudo, every git push, git reset --hard, git clean -f, curl | sh and a short list more.

It isn’t a sandbox. A command in the worktree can still reach the network and read files it shouldn’t want, and the deny list is a set of regexes: a speed bump, not a defence. What px does guarantee is that my own branch and uncommitted work are out of reach, and that comes from git, not from pattern matching.

All writing