npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cost-monitor/sdk

v0.1.1

Published

Typed client for cost-monitor: report LLM events and read back a project's events feed and spend metrics.

Readme

@cost-monitor/sdk

Typed client for cost-monitor. Report LLM events and read a project's own data back — all authenticated with a single project API key (cm_live_…) and scoped to that key's project.

  • Zero runtime dependencies.
  • ESM, Node ≥ 18 (uses the global fetch and crypto.randomUUID).
  • No automatic retries — a non-ok response or transport failure throws a typed CostMonitorError.

Install

npm install @cost-monitor/sdk
# or: pnpm add @cost-monitor/sdk

Usage

cost-monitor is self-hosted, so baseUrl is required — there is no default host. The API key is sent as Authorization: Bearer <apiKey>.

import { CostMonitorClient } from "@cost-monitor/sdk";

const client = new CostMonitorClient({
  apiKey: process.env.COST_MONITOR_API_KEY!, // cm_live_…
  baseUrl: "https://costs.your-company.com",
});

Report an event

const result = await client.track({
  provider: "openai",
  model: "gpt-4o",
  inputTokens: 1200,
  outputTokens: 350,
  status: "success",
  feature: "chat",
  customerId: "cust_123",
  // clientEventId is optional — the SDK fills it with a UUID for idempotency.
});
// → { id, created, costUsd, warnings? }

Report many events at once

Up to 100 events per call. Per-item failures are reported in the result rather than throwing.

const { results } = await client.trackBatch([
  { provider: "openai", model: "gpt-4o", inputTokens: 10, outputTokens: 5, status: "success" },
  { provider: "anthropic", model: "claude-sonnet-4-5", inputTokens: 8, outputTokens: 4, status: "success" },
]);

Buffered delivery (fire-and-forget)

client.track / client.trackBatch send immediately and throw on failure — you decide when and how to retry. For a long-lived process that would rather "enqueue and forget", wrap the client in an EventBuffer. It batches events (flush by size or interval) and retries transient failures (5xx, network, 429) with exponential backoff; 400 / 401 / 413 are dropped, since a retry can't fix them. A clientEventId is assigned at enqueue and reused across retries, so the server deduplicates and no event is double-counted.

import { CostMonitorClient, EventBuffer } from "@cost-monitor/sdk";

const client = new CostMonitorClient({ apiKey, baseUrl });
const buffer = new EventBuffer(client, {
  maxBatchSize: 50, // flush when this many are queued (default 50, max 100)
  flushIntervalMs: 5000, // …or this often (default 5s)
  maxQueueSize: 10_000, // drop-oldest beyond this (default 10k)
  maxRetries: 5, // retries after the first attempt (default 5)
  onError: (event, err) => log.warn({ event, err }, "event dropped"),
  onDrop: (event) => log.warn({ event }, "queue overflow"),
});

buffer.enqueue(event); // synchronous, never throws

enqueue never throws and onError / onDrop exceptions are swallowed, so the buffer can't break your hot path. The interval timer is unref'd and won't keep the process alive on its own. Flush remaining events before exit:

process.on("SIGTERM", async () => {
  await buffer.close(); // stops the timer + final flush; idempotent
  process.exit(0);
});

Auto-tracking via wrappers

wrapOpenAI / wrapAnthropic return a transparent proxy of your provider client that reports each completion to a sink for you — you keep calling the provider SDK exactly as before. The response (and stream) you get back is untouched; the server stays the source of truth for cost. openai and @anthropic-ai/sdk are optional peer dependencies — install only the one you use.

import OpenAI from "openai";
import { CostMonitorClient, EventBuffer, wrapOpenAI, withCostMonitor } from "@cost-monitor/sdk";

const buffer = new EventBuffer(new CostMonitorClient({ apiKey, baseUrl }));
const openai = wrapOpenAI(new OpenAI(), { sink: buffer, defaultFeature: "chat" });

// Plain call — attribution comes from the wrap-config defaults:
await openai.chat.completions.create({ model: "gpt-4o", messages });

// Per-call attribution via withCostMonitor (stripped before the request hits OpenAI):
await openai.chat.completions.create(
  withCostMonitor({ model: "gpt-4o", messages }, { customerId: "cust_123", feature: "summarize" }),
);

The sink is anything with enqueue(event) — an EventBuffer is the usual choice. Per-call feature / customerId / userId / metadata go through withCostMonitor, which stashes them under a private Symbol that never reaches the provider. Streaming works too (consume the stream as usual; usage is read from the final chunk — for OpenAI stream_options.include_usage is enabled for you). If the provider call throws, an event with status: "error" is recorded and the original error is re-thrown unchanged. Tracking never throws into your call path; route internal failures with onError.

Anthropic is identical via wrapAnthropic(new Anthropic(), { sink }) (intercepts messages.create). Do not wrap a client twice.

Read the events feed

Keyset-paginated, newest-first by default. Pass the returned nextCursor back as cursor to page; nextCursor: null means the end of the feed.

let cursor: string | undefined;
do {
  const page = await client.getEvents({ cursor });
  for (const event of page.items) {
    console.log(event.createdAt, event.model, event.costUsd);
  }
  cursor = page.nextCursor ?? undefined;
} while (cursor);

Read spend metrics

The bundled dashboard metrics for a period ("7d" | "30d" | "90d", default "30d"): total, top features/customers/models by spend, and a daily time series.

const metrics = await client.getMetrics({ period: "7d" });
console.log(metrics.total.costUsd, metrics.total.calls);
console.log(metrics.byModel); // [{ provider, model, costUsd, calls }, …]

Errors

All failures throw a subclass of CostMonitorError:

| Class | When | | --- | --- | | CostMonitorValidationError | 400 — invalid request | | CostMonitorAuthError | 401 — missing/invalid API key | | CostMonitorRateLimitError | 429 — carries retryAfter | | CostMonitorServerError | 413, 5xx, or a transport failure (status: 0) |

import { CostMonitorRateLimitError } from "@cost-monitor/sdk";

try {
  await client.track(event);
} catch (err) {
  if (err instanceof CostMonitorRateLimitError) {
    // back off for err.retryAfter seconds
  }
}

Publishing (maintainers)

Manual, pre-flight checked. The @cost-monitor npm scope availability is TBD — confirm before the first publish.

  1. npm whoami — confirm you are logged in.
  2. npm org ls cost-monitor / npm access ls-packages — is the @cost-monitor scope yours?
    • Scope free → npm org create cost-monitor (or publish under a personal scope).
    • If @cost-monitor is taken, fall back to the unscoped name cost-monitor-sdk — change name in package.json accordingly.
  3. pnpm --filter @cost-monitor/sdk build
  4. pnpm --filter @cost-monitor/sdk pack → inspect the tarball; its dist/ must have no real @cost-monitor/shared imports (only the inline comment in shared-types.js mentions the name).
  5. npm publish --dry-run (from packages/sdk) → review the files list.
  6. npm publishpublishConfig.access: "public" is already set.

License

MIT