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

@nullspend/sdk

v0.4.0

Published

TypeScript SDK for NullSpend — propose, approve, and execute risky AI agent actions

Readme

@nullspend/sdk

TypeScript SDK for NullSpend — propose, approve, and execute risky AI agent actions.

Quick start

import { NullSpend } from "@nullspend/sdk";

const seam = new NullSpend({
  baseUrl: "http://localhost:3000",
  apiKey: "ns_live_sk_...", // from your Settings page
});

const result = await seam.proposeAndWait({
  agentId: "my-agent",
  actionType: "send_email",
  payload: { to: "[email protected]", subject: "Follow up" },
  execute: async () => {
    return await sendEmail("[email protected]", "Follow up", "...");
  },
});

The SDK creates a pending action in NullSpend, polls until a human approves or rejects it, then either runs your execute callback or throws.

API

new NullSpend(config)

| Option | Type | Required | Description | | ------------------ | -------- | -------- | ------------------------------------------------ | | baseUrl | string | Yes | URL of your NullSpend instance | | apiKey | string | Yes | API key from Settings | | fetch | fetch | No | Custom fetch implementation (defaults to global) | | requestTimeoutMs | number | No | Per-request timeout in ms (default: 30000). Set to 0 to disable. |

seam.proposeAndWait(options)

High-level: proposes an action, waits for approval, executes, and reports the result.

| Option | Type | Default | Description | | ---------------- | ----------------------------- | -------- | ---------------------------------- | | agentId | string | required | Identifier for the agent | | actionType | string | required | e.g. send_email, http_post | | payload | Record<string, unknown> | required | Action details shown in the inbox | | metadata | Record<string, unknown> | optional | Extra context (environment, etc.) | | execute | (context?: ExecuteContext) => T \| Promise<T> | required | Runs only if approved. Receives { actionId } for cost correlation. | | expiresInSeconds | number \| null | optional | Server-side TTL. Omit for default (1 hour). Set to 0 or null for never-expire. | | pollIntervalMs | number | 2000 | ms between status polls | | timeoutMs | number | 300000 | Total timeout in ms | | onPoll | (action: ActionRecord) => void | optional | Called each poll cycle |

Lower-level methods

// createAction returns { id, status: "pending", expiresAt: string | null }
const action = await seam.createAction({ agentId, actionType, payload, expiresInSeconds: 600 });
const fetched = await seam.getAction(actionId);
const decided = await seam.waitForDecision(actionId, { pollIntervalMs, timeoutMs });
await seam.markResult(actionId, { status: "executed", result: { ... } });

Cost correlation

When using the NullSpend proxy for LLM calls, pass the actionId from the execute context as a header to link cost events to the action:

const result = await seam.proposeAndWait({
  agentId: "my-agent",
  actionType: "http_post",
  payload: { prompt: "Summarize this document" },
  execute: async (context) => {
    const res = await fetch("https://proxy.nullspend.dev/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-nullspend-action-id": context?.actionId ?? "",
      },
      body: JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hello" }] }),
    });
    return res.json();
  },
});

The linked cost data will appear on the action detail page in the dashboard.

Tracked Fetch (Provider Wrappers)

Wrap your LLM provider's fetch to automatically track costs and enforce policies client-side:

import { NullSpend, SessionLimitExceededError, BudgetExceededError, MandateViolationError } from "@nullspend/sdk";
import OpenAI from "openai";

const ns = new NullSpend({
  baseUrl: "https://app.nullspend.dev",
  apiKey: "ns_live_sk_...",
  costReporting: {},
});

// Basic cost tracking
const openai = new OpenAI({ fetch: ns.createTrackedFetch("openai") });

// With enforcement: budget, mandates, and session limits
const enforced = new OpenAI({
  fetch: ns.createTrackedFetch("openai", {
    enforcement: true,
    sessionId: "task-042",
    sessionLimitMicrodollars: 5_000_000, // $5 per session
    tags: { team: "backend" },
  }),
});

Providers: "openai" and "anthropic". Cost is calculated locally using the built-in pricing engine. With enforcement: true, the SDK checks model mandates, budget, and session limits before each request — throwing MandateViolationError, BudgetExceededError, or SessionLimitExceededError if policy is violated.

When using the proxy, the SDK also intercepts proxy-side 429 denials and throws typed errors:

| Error | Proxy code | When | |---|---|---| | BudgetExceededError | budget_exceeded | Key/user/org budget exhausted | | BudgetExceededError (entityType: "customer") | customer_budget_exceeded | Customer-scoped budget exhausted | | VelocityExceededError | velocity_exceeded | Spend rate exceeds velocity limit | | SessionLimitExceededError | session_limit_exceeded | Session spend cap reached | | TagBudgetExceededError | tag_budget_exceeded | Tag-level budget exhausted | | LoopDetectedError | loop_detected | Same model+content called 50+ times in 60s |

All denial types fire the onDenied callback before throwing. Proxy 429s with non-NullSpend codes (e.g. an upstream OpenAI rate limit forwarded through the proxy) pass through as raw Response objects.

Every denial error includes an optional recovery property with machine-readable hints:

try {
  await trackedFetch(url, init);
} catch (err) {
  if (err instanceof BudgetExceededError) {
    console.log(err.recovery?.retryable);          // false
    console.log(err.recovery?.ownerActionRequired); // true — needs human/config action
    console.log(err.recovery?.retryAfterSeconds);   // null (not retryable)
    console.log(err.recovery?.docs);                // docs URL or null
  }
}

The Recovery type is exported from @nullspend/sdk for consumers who want to type it. The recovery property is undefined when connecting to an older proxy that doesn't include it.

Proxy detection — the SDK takes the proxied path (skipping client-side cost tracking to avoid double-counting against the proxy's own write) when EITHER:

  1. proxyUrl is set on the NullSpend constructor AND the request URL's origin matches it (strict on scheme + host + port — include the port if your proxy uses a non-default one), OR
  2. The request carries an x-nullspend-key header (set on either init.headers or the underlying Request object's headers).

See the full SDK docs for TrackedFetchOptions reference.

Error handling

import {
  RejectedError,
  TimeoutError,
  NullSpendError,
  BudgetExceededError,
  LoopDetectedError,
  MandateViolationError,
  SessionLimitExceededError,
  VelocityExceededError,
  TagBudgetExceededError,
} from "@nullspend/sdk";

try {
  await seam.proposeAndWait({ ... });
} catch (err) {
  if (err instanceof RejectedError) {
    // Human rejected (or action expired)
  } else if (err instanceof TimeoutError) {
    // No decision within timeoutMs
  } else if (err instanceof LoopDetectedError) {
    // Agent stuck in loop — err.model, err.callCount, err.detectionType
    // err.recovery?.retryable is true (retry after backoff)
  } else if (err instanceof VelocityExceededError) {
    // Spending too fast — err.retryAfterSeconds, err.limitMicrodollars
    // err.recovery?.retryable is true, err.recovery?.retryAfterSeconds has the wait
  } else if (err instanceof TagBudgetExceededError) {
    // Tag budget exhausted — err.tagKey, err.tagValue, err.remainingMicrodollars
    // err.recovery?.ownerActionRequired is true
  } else if (err instanceof NullSpendError) {
    // API error (err.statusCode has the HTTP status)
  }
}

Loop Detection

Detects agents stuck in infinite loops — repeated identical calls that burn budget without progress.

Proxy users: Loop detection is on by default. If your agent calls the same model with identical content 50+ times in 60 seconds, the proxy returns a 429 with code: "loop_detected". No configuration needed.

SDK users: Opt in with one line:

const openai = new OpenAI({
  fetch: ns.createTrackedFetch("openai", { loopDetection: true }),
});

Customize thresholds:

const openai = new OpenAI({
  fetch: ns.createTrackedFetch("openai", {
    loopDetection: {
      maxCalls: 100,        // higher for batch workloads
      windowSeconds: 120,   // wider window
    },
  }),
});

Disabling: Set loopMaxCalls: 0 on the budget entity via the API or dashboard.

Multi-model detection: If 5+ distinct model patterns each show 3+ repeated calls in the window, aggregate loop detection triggers — catching agents that alternate between models while stuck.

Demos

Three runnable demos in examples/ show the approval loop for different action types:

| Demo | Action type | What it does | |---|---|---| | demo-send-email.ts | send_email | Simulates sending an email (no real mail sent) | | demo-http-post.ts | http_post | POSTs a CRM lead payload to a real public API | | demo-shell-command.ts | shell_command | Executes a safe shell command on the host |

# Terminal 1: start the app
pnpm dev

# Terminal 2: run any demo
NULLSPEND_API_KEY=ns_live_sk_... pnpm tsx packages/sdk/examples/demo-send-email.ts
NULLSPEND_API_KEY=ns_live_sk_... pnpm tsx packages/sdk/examples/demo-http-post.ts
NULLSPEND_API_KEY=ns_live_sk_... pnpm tsx packages/sdk/examples/demo-shell-command.ts

Then open http://localhost:3000/app/inbox and approve the action.