Bundle
dsh-spend-receipt
DeepSeek Harness plugin: derive a citable append-only JSONL cost receipt from usage events or a session log, and report it through the spend_receipt tool
- Source
- jwilson411
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-spend-receipt
A **DeepSeek Harness function plugin** that turns reported usage into a
citable, append-only JSONL cost receipt, and a `spend_receipt` tool that reads
it back: the last N lines, the totals, and the cache-hit rate.
The point is that the number is checkable. Every line records the token counts
a provider actually reported, the dated rate table it was priced under, which
side of the off-peak window it fell on, and where it was derived from — so a
reader can re-derive the cost without trusting this package. Nothing is
estimated: no text is tokenised, no missing count is filled in with a zero, and
a call that cannot be priced is listed with its tokens and `usd: null` rather
than a guess.
## Install
```sh
dsh plugin --profile web add github:jwilson411/dsh-spend-receipt
```
`dsh plugin` forwards to pnpm inside `$DSH_HOME/profiles/web`, then reconciles
the profile: because this package's manifest declares `dsh.bundle.patch`, it is
appended to the profile manifest's ordered `dsh.profile.bundles` list and its
`cordis.patch.yml` becomes a layer. Remove it the same way, with `remove` in
place of `add`.
Point it at your paths from the profile's own `cordis.patch.yml` — note that an
id-targeted patch replaces the row's whole `config`, so restate every field you
mean to keep:
```yaml
- id: spend-receipt
config:
receiptPath: ~/.dsh/spend-receipt.jsonl
sessionLog: ~/.dsh/sessions/current.jsonl
```
| config key | environment fallback | default |
|---|---|---|
| `receiptPath` | `DSH_SPEND_RECEIPT_PATH` | `./spend-receipt.jsonl` |
| `sessionLog` | `DSH_SPEND_RECEIPT_SESSION_LOG` | unset — read-only mode |
| `sessionId` | `DSH_SPEND_RECEIPT_SESSION_ID` | unset — lines record `null` |
| `limit` | — | `20` |
## Input assumptions
Receipt lines come from one of two places, never from a guess.
**Usage events.** A host that already has a usage hook calls `recordUsage(event,
{ receiptPath })`; the CLI's `record` command does the same for JSONL on stdin.
An event is anything with a model and token counts, at the top level or nested
under `usage`, `usage.details`, or `response.usage`. Both snake and camel
spellings are read (`prompt_tokens` / `input_tokens` / `promptTokens`,
`prompt_cache_hit_tokens` / `cache_hit_tokens` / `cached_tokens` under
`prompt_tokens_details`, `completion_tokens` / `output_tokens`). If an event
reports the cache hit and miss halves but no prompt total, the total is their
sum — the one derivation allowed, because it restates reported counts rather
than measuring anything new.
**An append-only session log.** `syncFromSessionLog({ logPath, receiptPath })`
reads the log as JSONL: lines carrying usage become receipt lines, a line
carrying only a session id sets the session for the lines below it, and
everything else is ignored. The log must be genuinely append-only — resumption
has no separate cursor file, it is the highest log line the receipt already
cites, so a rewritten log needs a fresh receipt. Re-running a sync over a
growing log appends only what is new.
**The receipt** is JSONL and is only ever appended to; a batch is serialised and
written in one append so a crash cannot interleave a half-written line. One
corrupt line is counted and reported, not thrown — it cannot make the rest of
the history unreadable.
One line, formatted:
```json
{
"ts": "2026-08-29T09:00:12.000Z",
"session_id": "sess-7c2",
"model": "deepseek-v4-pro",
"input_tokens": 12000,
"cache_hit_tokens": 9000,
"output_tokens": 800,
"usd": 0.007524,
"currency": "USD",
"nano_usd": 7524000,
"rate_table": "deepseek-v4@2026-08-29",
"rate_tier": "peak",
"ts_source": "event",
"source": { "kind": "session-log", "path": "…/session.jsonl", "line": 3 }
}
```
`input_tokens` is the total billed prompt, of which `cache_hit_tokens` were
served from cache. `nano_usd` is the exact integer cost in 1e-9 USD that `usd`
rounds; totals are summed from it, so a thousand-line receipt adds up without
floating-point drift. `ts_source` is `event` when the timestamp came from the
usage event and `recorded` when the event carried none and this is when the line
was written.
## The rate table
Pinned and dated **2026-08-29**, read from DeepSeek's published price list:
<https://api-docs.deepseek.com/quick_start/pricing>. Bump `RATE_TABLE_DATE` and
the id in `src/rates.js` when the rates change, so old lines stay attributable
to the numbers that actually produced them.
USD per 1M tokens:
| model | tier | cache hit | cache miss | output |
|---|---|---|---|---|
| `deepseek-v4-pro` | peak | $0.044 | $1.32 | $3.96 |
| `deepseek-v4-pro` | off-peak | $0.022 | $0.66 | $1.98 |
| `deepseek-v4-flash` | peak | $0.014 | $0.44 | $1.32 |
| `deepseek-v4-flash` | off-peak | $0.007 | $0.22 | $0.66 |
**Peak is 01:00–04:00 and 06:00–10:00 UTC, Monday through Friday.** Every other
hour is off-peak, at half the peak rate — the gap between the two windows, every
evening and night, and the whole of Saturday and Sunday. Peak is the narrow
case, so a call is off-peak unless it lands inside one of those two weekday
windows.
Windows are half-open, `[start, end)`: 04:00:00 is already off-peak. The tier is
read from the call's own timestamp, in UTC, whatever zone the offset was written
in — `2026-08-29T03:00:00+02:00` is 01:00 UTC on a Saturday, so it is off-peak
despite reading like a peak hour locally.
Rates are stored as integer nano-USD per token rather than floats, so pricing
and totalling are integer arithmetic and the conversion to USD happens once, at
the edge.
`deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`, and the two `-latest`
pointers resolve to the rows above. Only spellings of the *same* model are
aliased — mapping a near neighbour onto a priced row would invent a price.
### When a call cannot be priced
The tokens are still recorded, `usd` and `nano_usd` are `null`, and
`unpriced_reason` says which of three things was missing:
- `model-not-in-rate-table` — an unknown or newer model. Its tokens appear in
the token totals and it is counted in `unpriced_lines`, but it contributes
nothing to `usd`, so a total understates rather than invents.
- `cache-split-unknown` — a prompt total with no reported cache split. The two
halves bill at rates ~30× apart, so the call cannot be priced from the total
alone. Such lines are also excluded from *both* halves of the cache-hit rate,
rather than being counted as misses.
- `timestamp-unknown` — no usable instant, so the peak/off-peak tier cannot be
determined. An unknown tier does not silently become the peak one.
## The `spend_receipt` tool
| | |
|---|---|
| Cordis plugin id | `spend-receipt` (the row id in `cordis.patch.yml`) |
| Injects | `tools` — a hard dependency; the plugin waits rather than degrading |
| Tool | `spend_receipt` |
| Arguments | `limit` (integer, optional), `sync` (boolean, optional) |
`limit` is how many of the most recent receipt lines to return, defaulting to
the configured `limit`. `sync` defaults to true: the tool first sweeps the
configured `sessionLog` for new usage, then reports. Pass `sync: false` to
report only what has already been recorded. A sweep that cannot run — a
mistyped or not-yet-created log — reports its failure in `synced.error` rather
than making the receipt unreadable.
It returns `receipt_path`, the `rate_table` it priced under, the `lines`
themselves, `totals` and `cache_hit_rate` over the whole receipt,
`window_totals` and `window_cache_hit_rate` over just the returned lines,
`malformed_lines`, and `synced`. The cache-hit rate is
`cache_hit_tokens / input_tokens` over lines with a known split, with a `basis`
saying how much of the receipt it was computed over.
The tool reads a file and reaches no network, so it needs no API key.
## The CLI
The same functions from a shell. It imports nothing outside `node:` and this
package, so it runs against a checkout with no dependencies installed — useful
when the receipt is the thing you need and the harness is the thing that is
broken.
```sh
dsh-spend-receipt show [--receipt <path>] [-n <count>] [--json]
dsh-spend-receipt sync --log <path> [--receipt <path>] [--session <id>] [--json]
dsh-spend-receipt record [--receipt <path>] [--session <id>] [--json] # events on stdin
dsh-spend-receipt rates [--json]
```
```
$ dsh-spend-receipt sync --log ~/.dsh/sessions/current.jsonl
appended 6 line(s) to /home/you/spend-receipt.jsonl
from /home/you/.dsh/sessions/current.jsonl, 12 new log line(s) scanned, cursor now 12
skipped log line 6: unparseable-json
skipped log line 9: invalid-count
$ dsh-spend-receipt show -n 2
receipt: /home/you/spend-receipt.jsonl
2026-08-29T20:15:00.000Z deepseek/deepseek-v4-pro off-peak in 2000 (cached 0) out 100 $0.001518
2026-08-29T12:00:00.000Z deepseek-v4-flash peak in 100 (cached 50) out 10 $0.000036
showing 2 of 6 line(s)
totals (whole receipt): $0.009491 USD over 19600 input (13050 cached) and 1210 output tokens
cache-hit rate: 68.0% (over 5 line(s) with a known split)
unpriced: 2 line(s) contributed tokens but no cost
```
`rates` prints the pinned table, which is the fastest way to check what a
receipt was priced against.
## Layout
```
package.json manifest + `dsh.bundle.patch` — what makes this a bundle
cordis.patch.yml the bundle's patch layer: one insert, one plugin row
src/rates.js the pinned, dated rate table and exact nano-USD pricing
src/usage.js reading reported counts without inventing any
src/receipt.js building, appending, reading, and syncing receipt lines
src/summary.js the last N lines, totals, and cache-hit rate
src/index.js the plugin: `name`, `inject`, `apply(ctx, config)`
bin/ the CLI
test/ offline tests over checked-in fixtures
package-lock.json the pinned dependency tree `npm ci` installs in CI
```
## Tests
```sh
npm install
npm test
```
Offline by construction, and fixture-only. The suite reads the checked-in JSONL
fixtures in `test/fixtures/` and writes scratch receipts to a temporary
directory: no profile boots, no socket opens, no key is read, and no clock is
consulted — the recording time is passed in, so a line whose event carried no
timestamp is still deterministic.
Only `test/plugin.test.js` needs a dependency: it registers the plugin against a
stub context and validates the tool's result with the real
`@deepseek-ai/dsh-tools`, pinned to `0.1.1-rc.2` in `devDependencies` and in
`package-lock.json` so the contract is tested against one known API. Everything
else — the library, the CLI, and the other four test files — imports nothing
outside `node:` and this package.
CI (`.github/workflows/ci.yml`) runs `npm ci` and `npm test` on Node 22 and 24
from the committed lockfile, against the public registry only. It needs no
credentials and the suite reaches no network.
## License
MIT — see [LICENSE](LICENSE).
Install
dsh plugin --profile web add github:jwilson411/dsh-spend-receipt
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-spend-receipt from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.