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

@budgetary/sdk

v0.7.0

Published

TypeScript SDK for the Budgetary API.

Downloads

1,781

Readme

@budgetary/sdk

TypeScript SDK for the Budgetary API — a hosted service that returns probabilistic pre-inference estimates of token spend for LLM queries. The SDK is a thin, typed HTTP wrapper with zero runtime dependencies; it speaks the v1 contract documented at docs/api-contract.md.

Install

npm install @budgetary/sdk
# or: pnpm add @budgetary/sdk   /   yarn add @budgetary/sdk

The package publishes both ESM and CommonJS builds with type declarations for each, so import and require both work from TypeScript and JavaScript.

Quickstart

import { BudgetaryClient, normalizeScenario } from "@budgetary/sdk";

// A free `bg_test_` key works immediately for development; `bg_live_` is the
// production key (and must be on an active plan). Get one at https://budgetary.tools
const client = new BudgetaryClient({ apiKey: process.env.BUDGETARY_API_KEY! });

const estimate = await client.estimate("fix the flaky test in the payments service", {
  model: "claude-opus-4-7",
  context: { host: "sdk", projectId: "proj_kx7" },
});

if (estimate.void || estimate.distribution === null) {
  // The server declined to estimate (out of domain). Not an error.
  console.log("No confident estimate for this query.");
} else {
  const { p10, p50, p90 } = estimate.distribution;
  console.log(
    `~${p50} tokens (p10–p90: ${p10}–${p90}), scenario: ${normalizeScenario(estimate.scenario)}`,
  );
}

Reading an estimate

A successful estimate is a range, not a point. distribution gives p10, p50, and p90 combined input+output tokens; treat p50 as the midpoint of that range, not a guaranteed cost. Three fields tell you how much to trust it:

  • scenario (contract §5) — confident (the range is reliable), uncertain (supported but wide), sparse_evidence (near the edge of what's been seen), or out_of_domain (returned with void: true). The server may add labels at any time, so fold unknown values with normalizeScenario(scenario), which maps anything unrecognized to "uncertain" — never treat an unknown label as confident.
  • confidence — a single [0, 1] quality summary. Lower means a wider range and a rougher midpoint. Read it alongside scenario, not as a probability.
  • voidtrue when the server declined to estimate (out of domain). Branch on it before reading distribution, which is null in that case. A void response is not an error.

Don't render a low-confidence or void estimate as if it were a precise number — surface the range and the caveat.

The idiomatic read narrows distribution by branching on void first, then destructures in the non-null branch:

if (estimate.void || estimate.distribution === null) {
  // no prediction — render the caveat
} else {
  const { p10, p50, p90 } = estimate.distribution; // non-null here
}

Closing the loop

After a run, submit the realized counts so future estimates calibrate, then read them back from the ledger. Token counts must be measured, never guessed.

// Submit realized usage for a prior estimate (idempotent on estimateId).
await client.submitActuals({
  estimateId: estimate.estimateId,
  tokensIn: 12_340,
  tokensOut: 36_210,
  success: true,
  durationMs: 420_000,
});

// Read the predicted-vs-actual ledger.
const page = await client.getLedger({ projectId: "proj_kx7", limit: 50 });
for (const entry of page.entries) {
  console.log(entry.queryExcerpt, entry.predicted.p50, entry.actual?.total ?? "pending");
}

submitActuals returns 202 and is idempotent on estimateId (safe to retry on a network failure). getLedger is paginated — pass the returned nextCursor as after for the next page, and set includeOrphans: true to include estimates with no actuals yet.

Error handling

Every documented HTTP status maps to a typed exception. Retryable errors (429, 5xx) are retried automatically with exponential backoff and jitter — by the time an error reaches your code, retries have already been exhausted.

import {
  BudgetaryClient,
  BudgetaryRateLimitError,
  BudgetaryValidationError,
} from "@budgetary/sdk";

try {
  await client.estimate(query);
} catch (err) {
  if (err instanceof BudgetaryRateLimitError) {
    console.warn(`rate limited; retry after ${err.retryAfterSeconds}s`);
  } else if (err instanceof BudgetaryValidationError) {
    console.error(`bad input: ${err.message}`);
  } else {
    throw err;
  }
}

The full hierarchy:

  • BudgetaryError — base class (code, httpStatus, requestId).
  • BudgetaryAuthError401 (key missing, invalid, or revoked — re-authenticate).
  • BudgetaryPermissionError403 (valid key, but lacks scope or an active plan — distinct from 401).
  • BudgetaryNotFoundError404.
  • BudgetaryValidationError400, 409, 413.
  • BudgetaryRateLimitError429; exposes retryAfterSeconds.
  • BudgetaryServerError5xx.
  • BudgetaryNetworkError — no response received (timeout, network, abort).

Constructor options

| Option | Type | Default | Notes | |---|---|---|---| | apiKey | string | required | Sent as Authorization: Bearer <apiKey>. | | baseUrl | string | https://api.budgetary.tools | Override for staging or self-hosted endpoints. | | timeoutMs | number | 10_000 | Per-request timeout via AbortSignal.timeout. | | maxRetries | number | 4 | Maximum retries on 429 and 5xx. Total attempts = maxRetries + 1 (5, per contract §8). | | fetchImpl | typeof fetch | global fetch | Inject a mock fetch in tests. |

Idempotency

estimate() accepts an optional clientRequestId that is forwarded to the server as client_request_id. The API treats identical replays within 24 hours as the same call, returning the original response without rebilling. The SDK auto-generates a fresh UUID v4 on every call by default so retries are safe out of the box.

// default — SDK generates a UUID
await client.estimate("...");

// caller-supplied
await client.estimate("...", { clientRequestId: "my-deterministic-id" });

// explicit opt-out — no client_request_id sent
await client.estimate("...", { clientRequestId: null });

Naming conventions

  • Wire protocol uses snake_case (estimate_id, tokens_in, client_request_id).
  • SDK surface uses camelCase (estimateId, tokensIn, clientRequestId).

The SDK converts at the HTTP boundary in both directions; callers never see snake_case.

Reference

For the full API contract — endpoints, error codes, scenario labels, idempotency semantics — see docs/api-contract.md.

Licensed under Apache-2.0.