Bundle
@zoytown/dsh-billing
DeepSeek Harness (dsh) plugin for DeepSeek API account balance — a ctx.billing capability seam, a /balance command, and a sidebar balance indicator with a Settings page for the Web UI.
- Source
- zoyluoblue
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 5 days ago
Readme
# @zoytown/dsh-billing
English | [中文](README.zh.md)

`@zoytown/dsh-billing` is a **[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugin that shows your DeepSeek API account balance**. It reads the platform's `GET /user/balance` endpoint and surfaces the result three ways: a capsule at the sidebar foot, a Balance section in Settings, and the `/balance` command. It registers **no model-facing tool** and appends **no session event**, so mounting it costs the conversation nothing.
| Sidebar | Settings → Balance |
|---|---|
|  |  |
## What the platform actually exposes
Only the current balance. There is no usage or spend-history endpoint — `/usage` and `/dashboard/billing/usage` both return 404 — so this package reports **what is left**, never **what was spent**. Any per-session cost figure would be a local estimate from token counts, which is a separate concern and deliberately out of scope here.
```json
{
"is_available": true,
"balance_infos": [
{ "currency": "CNY", "total_balance": "25.00", "granted_balance": "0.00", "topped_up_balance": "25.00" }
]
}
```
`balance_infos` is an array — an account can hold CNY and USD at once — and every consumer here renders all of it rather than the first entry.
## Install
```sh
dsh plugin --profile web add @zoytown/dsh-billing
```
Installing from npm gets prebuilt code and needs no build permission. A git install (`github:zoyluoblue/deepseek-harness-billing`) fetches sources without running a build, so it is **not** supported yet — see [Known limitations](#known-limitations).
The bundle inserts three rows — the service (which is also the browser row), the `/balance` command, and the UI's data route. Each is useful without the others; disable any of them by id in your profile's `cordis.patch.yml`.
## Config
| Key | Default | Meaning |
|---|---|---|
| `apiKey` | omitted | Literal key. Prefer `apiKeyEnv` so no secret enters configuration; a non-empty literal wins. Carries `role('secret')`, so it never rides a `describe()` response. |
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | Credential reference resolved through `ctx.credentials` **per read**, or from the launch environment when that seam is absent. Reuses the LLM adapter's key — this package adds no new secret. |
| `baseURL` | `https://api.deepseek.com` | Billing endpoint base; `/user/balance` is appended. Falls back to `$DEEPSEEK_BILLING_BASE_URL`. |
| `cacheTtlMs` | `60000` | How long a successful snapshot stays fresh. |
| `timeoutMs` | `10000` | Per-request abort ceiling. |
| `lowBalanceThreshold` | `10` | Warn below this amount. `0` disables the floor, leaving the platform's own `is_available` verdict. |
### Why not `$DEEPSEEK_BASE_URL`
That variable steers the chat-completions adapter, and users legitimately point it at gateways and self-hosted endpoints. `/user/balance` exists **only** on the official platform, so reusing it would turn a working proxy setup into a permanent 404 in the sidebar. The endpoint gets its own variable, exactly as [`dsh-web-search-deepseek`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/web/web-search-deepseek) gives its own to search.
A `baseURL` that does not serve the path fails as `ENDPOINT_UNAVAILABLE` with a message naming this cause, rather than as a generic HTTP error.
## Caching
One policy in one place, because three surfaces can ask at the same moment and this is an account endpoint with no documented rate limit:
- a successful snapshot is served for `cacheTtlMs`;
- concurrent asks share **one** in-flight request, and one caller's cancellation never cancels the read the others joined;
- a **failure is never cached** — the next ask retries, and the last good snapshot is retained so a surface can show it beside the error;
- a committed `credentials/updated` for this reference invalidates immediately.
There is **no background polling**. Freshness is driven by consumers asking.
## Errors
`BillingError.code` is the branch point for a UI. A failed read must never render as a zero balance: "out of money" and "could not tell" are different facts.
| Code | Cause |
|---|---|
| `CREDENTIAL_MISSING` | Nothing supplies the reference; no request is sent. |
| `UNAUTHORIZED` | HTTP 401/403. |
| `ENDPOINT_UNAVAILABLE` | HTTP 404 — almost always a `baseURL` pointing at a gateway. |
| `RATE_LIMITED` | HTTP 429. |
| `HTTP_ERROR` | Any other non-2xx. |
| `MALFORMED_RESPONSE` | HTTP 200 whose body is not a balance document. |
| `NETWORK_ERROR` | Transport failure, timeout, unparseable base, or a refused redirect. |
| `ABORTED` | The caller cancelled. |
Two wire details this handles that a naive client does not: the endpoint returns a JSON `error.message` for an invalid key but **plain text** when the `Authorization` header is absent entirely, so the body is never parsed as JSON unconditionally; and redirects are refused with `redirect: 'error'` before the `Location` target is contacted, because following one would carry the bearer token to another host.
## Commands
| Command | Effect |
|---|---|
| `/balance` | Render the balance, using the cache. |
| `/balance refresh` | Same, ignoring a fresh cache entry. |
## Web UI
Two browser surfaces, both backed by one shared controller so opening the settings page while the capsule is already reading joins that read instead of firing a second one.
| Surface | Slot | What it shows |
|---|---|---|
| Sidebar capsule | `sidebar.footer.action` | The amount beside Settings; a 32px icon with a status pip in the 56px rail |
| Settings → Balance | `settings.section` | Every currency, the granted/topped-up split, and the current threshold |
The capsule distinguishes five states, and the rule the whole design turns on is that **a failed read renders an em dash, never a number** — "out of money" and "could not tell" must not look alike. `unconfigured` shows a dashed outline and no digits at all; `low` is the only state allowed to draw the eye, and it pairs amber with a warning triangle so colour is never the sole signal. The rail pip appears only for `low` and `error`: a healthy balance has no business flashing in peripheral vision.
Styling uses `--dsw-alias-*` semantic tokens exclusively — this plugin defines no theme, ships no light/dark selectors, and inherits both themes from `ui-theme`.
### The data channel
The browser half reads `GET /billing/balance` from the `billing-route` row. It is a plain webserver route rather than a Typert Remote because a Remote needs a generated invocation descriptor from the harness's own codegen, which an out-of-tree package cannot produce.
That route answers account data, so it carries its own browser-trust fence against the two confused-deputy paths a local HTTP API opens — **DNS rebinding** (a page resolves its own domain to 127.0.0.1 so the socket reaches this server while `Host` names the attacker) and ordinary **cross-site reads**. `Host` must be loopback or listed in `trustedHosts`, and any attached Fetch-Metadata must say same-origin. It is deliberately stricter than the harness's own `/api` fence: no LAN-IP grants are derived, so anything past loopback must be named explicitly.
```yaml
- id: billing-route
config:
trustedHosts: [] # add "host" or "host:port" only for an off-machine deployment
```
This is not authentication. It stops a browser being used as a proxy into loopback; it does not identify callers.
## FAQ
### How do I check my DeepSeek API account balance?
Call `GET https://api.deepseek.com/user/balance` with an `Authorization: Bearer <DEEPSEEK_API_KEY>` header. It returns `is_available` plus a `balance_infos` array holding one entry per currency. This plugin wraps that endpoint for DeepSeek Harness so the balance appears in the sidebar, in Settings, and via `/balance`.
### How do I install a dsh plugin?
`dsh plugin --profile <name> add <package>`. For this one:
```sh
dsh plugin --profile web add @zoytown/dsh-billing
```
The command installs the package into the profile and appends its bundle to the profile's `dsh.profile.bundles` list. Remove it again with `dsh plugin --profile web remove @zoytown/dsh-billing`.
### Why does my balance show "—" instead of a number?
Because the read failed — the plugin never prints a number it does not have. An em dash means "could not tell", which is deliberately distinct from a zero balance. Open Settings → Balance to see the classified reason (invalid key, endpoint unavailable, rate limited, network error).
### Does this plugin show how much I have spent?
No. The DeepSeek platform exposes no usage or spend-history endpoint — `/usage` and `/dashboard/billing/usage` both return 404 — so this plugin reports only the remaining balance. Any per-session cost figure would be a local estimate derived from token counts, which this package deliberately does not do.
### Can I use it with a gateway or a self-hosted DeepSeek endpoint?
Only for the chat-completions traffic, not for the balance. `/user/balance` exists only on the official platform, so this plugin uses its own `baseURL` (falling back to `$DEEPSEEK_BILLING_BASE_URL`) and never reuses `$DEEPSEEK_BASE_URL`. A `baseURL` that does not serve the path fails as `ENDPOINT_UNAVAILABLE` with a message naming that cause.
### Do I need a second API key?
No. It resolves the same `DEEPSEEK_API_KEY` credential reference the LLM adapter uses, through `ctx.credentials`. Rotating the key in the Models page reaches the next balance query without a restart.
### Does it cost tokens?
No. It registers no model-facing tool, contributes no system-prompt section, and appends no session event. Command results are rendered by the UI adapter and never enter model history.
## Model Experience
None. This package registers no tool, contributes no system-prompt section, and appends no session event. Command results are rendered by the UI adapter and never enter model history.
#### Token effect
Zero. Neither registration nor invocation reaches a model request.
#### KV Cache effect
None; nothing here enters a request prefix.
## Known limitations
- **The warning floor is a bare number applied per currency, in that currency's own units.** On a mixed CNY/USD account a threshold of `10` chosen with CNY in mind also flags a `$8.40` balance. A per-currency map is the fix and is deferred until a real multi-currency account needs it; `lowBalanceThreshold: 0` disables the floor meanwhile.
- **No spend or usage reporting.** The platform exposes no such endpoint; see [above](#what-the-platform-actually-exposes).
- **No git-install support.** The package ships no `prepare` script, so `dsh plugin add github:…` would install sources without a build. Install from npm, or from a `pnpm pack` tarball; both carry prebuilt code and need no build permission.
- **The capsule refreshes rather than deep-linking.** Clicking it re-reads the balance; opening the Balance settings page from it would need an "open settings" service the settings shell does not expose.
- **The capsule shows one currency.** A sidebar pill has no room for several, so it renders the first currency the platform listed and the settings page renders all of them. It never sums across currencies — adding CNY to USD would be a fabricated number.
- **The browser half assumes a same-origin server.** It fetches a relative path, which the Web app satisfies; an Electron shell loading `file://` and bridging fetch over IPC would need its own transport.
- **Balance freshness is pull-driven.** With no polling, a balance that drops between two asks is not noticed until something asks again.
- **Amounts are never re-formatted.** The platform's decimal strings pass through to presentation untouched, so a currency the platform reports in an unexpected shape renders in that shape.
## License
MIT
Install
dsh plugin --profile web add github:zoyluoblue/deepseek-harness-billing
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 zoytown-dsh-billing from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.