TokenLedger
zh667
Relay-site attributed token usage for DeepSeek Harness — zero config, no credentials
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:zoyluoblue/deepseek-harness-billing
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
English | 中文

@zoytown/dsh-billing is a 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 |
|---|---|
![]() |
![]() |
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.
{
"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.
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.
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.
| 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. |
$DEEPSEEK_BASE_URLThat 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 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.
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:
cacheTtlMs;credentials/updated for this reference invalidates immediately.There is no background polling. Freshness is driven by consumers asking.
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.
| Command | Effect |
|---|---|
/balance |
Render the balance, using the cache. |
/balance refresh |
Same, ignoring a fresh cache entry. |
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 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.
- 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.
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.
dsh plugin --profile <name> add <package>. For this one:
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.
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).
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.
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.
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.
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.
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.
Zero. Neither registration nor invocation reaches a model request.
None; nothing here enters a request prefix.
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.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.file:// and bridging fetch over IPC would need its own transport.MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: billing。