@hameddk/anthropic-spend-collector
v0.1.0
Published
Pull real spend and token usage from Anthropic's Admin Usage and Cost Report APIs. Joins per-workspace cost data with per-model token counts. Pluggable auth, paginated, zero dependencies.
Maintainers
Readme
@hameddk/anthropic-spend-collector
Pull real spend and token usage from Anthropic's Admin Usage and Cost Report APIs. Joins per-workspace cost data with per-model token counts into a single normalized row stream.
- Calls
/v1/organizations/cost_reportfor USD spend (no pricing-table guesses) - Calls
/v1/organizations/usage_report/messagesfor token counts per model - Cursor-based pagination; tested across 90+ day ranges
- Caller supplies the Admin API key — no DB, no filesystem, no business logic
- Zero dependencies, ESM, Node ≥ 18
Status: 0.1.0 — early. Public API is stable for the documented surface.
Why
Most Anthropic usage tooling either (a) only reads token counts and guesses cost from a pricing table, or (b) reads bulk cost without per-model attribution. This collector calls both Admin endpoints and merges them so you get real USD spend broken down by model.
Install
npm install @hameddk/anthropic-spend-collectorQuick start
import { runCollector } from '@hameddk/anthropic-spend-collector';
const result = await runCollector({
apiKey: process.env.ANTHROPIC_ADMIN_KEY, // sk-ant-admin-…
from: '2026-04-01',
to: '2026-04-30',
});
if (!result.ok) {
console.error(`[${result.errorType}] ${result.error}`);
process.exit(1);
}
for (const row of result.rows) {
console.log(`${row.date} ${row.identity} ${row.tool} $${row.cost_usd?.toFixed(4)}`);
}API
runCollector({
apiKey: string, // required — Admin API key (sk-ant-admin-…)
from: 'YYYY-MM-DD', // required — UTC inclusive
to: 'YYYY-MM-DD', // required — UTC inclusive
workspaceId?: string, // optional — filter to a single workspace
baseUrl?: string, // testing only
fetch?: typeof fetch, // testing only
})Success result
{
ok: true,
rows: Array<{
date: 'YYYY-MM-DD',
identity: string | null, // workspace_id, or null for aggregate rows
identityType: 'workspace_id' | 'aggregate',
tool: string, // model name, or 'aggregate' for cost-only rows
tokens_input: number,
tokens_output: number,
cost_usd: number | null, // null only when cost_report failed and warning emitted
session_minutes: 0,
raw: { usage?: ..., cost?: ... }
}>,
meta: {
via: 'cost_report+usage_report',
pages_fetched: number,
warnings: string[], // e.g. 'cost_report failed: ...; rows will have cost_usd: null'
}
}Error result
{
ok: false,
error: string,
errorType: 'auth' | 'rate_limit' | 'not_found' | 'network' | 'parse' | 'config',
}errorType: 'auth' is fatal even if usage_report succeeded — your token is
invalid for at least one of the endpoints, so the result would be incomplete.
Cost attribution heuristic
cost_report returns USD totals per workspace (and per description /
cost_type, but the endpoint does not expose per-model spend directly).
usage_report/messages returns token counts per workspace and per model.
To produce per-model cost rows, this collector distributes each
(date, workspace) cost across that bucket's models proportional to total
tokens (tokens_input + tokens_output).
cost_for_model = workspace_day_cost × (model_tokens / workspace_day_tokens)This is the cleanest attribution available without finer-grained data from
Anthropic's API. The sum of distributed costs equals the original
cost_report total for that bucket exactly. If a cost_report row has no
matching tokens (e.g. a web_search-only day), the collector emits a single
row with tool: 'aggregate' and tokens_input: 0 — the cost is preserved,
just not attributed to a specific model.
If you need different attribution (uniform across models, weighted by
output-only, etc.), apply your own logic to the returned rows; the raw
cost_report and usage_report payloads are preserved on row.raw.
Authentication
Both endpoints require an Admin API key that starts with sk-ant-admin-,
created from the Claude Console under Settings → Admin keys. Standard
inference keys (sk-ant-api-…) return 401 on the organization endpoints —
this collector validates the prefix and rejects them up front with a clear
hint.
Pagination
Both endpoints use a next_page cursor. The collector follows it
transparently; you don't need to drive pagination yourself. Tested with 90-day
ranges that span 4+ pages per endpoint — see test/pagination.test.js.
Errors
import {
AnthropicSpendError, // base
AnthropicSpendConfigError, // bad args, wrong key prefix, missing dates
AnthropicSpendAuthError, // 401/403 from either endpoint
AnthropicSpendRateLimitError, // 429
AnthropicSpendApiError, // other HTTP / parse failures
} from '@hameddk/anthropic-spend-collector';Errors carry the original response body when available, never the request headers. Your API key is in the request — it is not echoed in errors.
Testing hooks
For testing only:
runCollector({
...,
baseUrl: 'http://localhost:8080', // override the Anthropic host
fetch: customFetch, // override fetch
})Do not use these in production.
What this library does not do
- Doesn't write to a database — return value is rows; persist them yourself.
- Doesn't translate workspace IDs or emails to display names — your adapter does that mapping.
- Doesn't fall back to local pricing tables —
cost_usdis real ornull. - Doesn't synthesize "per-developer split" rows when only aggregate data exists — what the API returns is what you get.
License
MIT © 2026 Hamed Sattari
