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

@lovieco/tokenops

v0.1.0

Published

TokenOps SDK — attribute LLM spend to your customers with one line.

Readme

@lovieco/tokenops

Attribute LLM spend to your customers with one line.

TokenOps records what each LLM call cost and which of your customers it was for, so you can see cost-vs-revenue per customer. This SDK sends usage events to the TokenOps ingest endpoint.

Install

npm install @lovieco/tokenops

Requires Node 18+ (the client uses the global fetch and node:crypto). No runtime dependencies. The package is ESM-only ("type": "module"); import it from ESM or a bundler, not via require().

Quick start — auto-instrument Anthropic

import { TokenOps, wrapAnthropic } from "@lovieco/tokenops";
import Anthropic from "@anthropic-ai/sdk";

const tokenops = new TokenOps({
  companyId: "<your-company-uuid>",
  secretKey: process.env.TOKENOPS_SECRET_KEY!, // sk_live_...
  ingestUrl: "https://api.lovie.co",
});

// One line. Every non-streaming call is now attributed.
const anthropic = wrapAnthropic(new Anthropic(), tokenops);

await anthropic.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "hi" }],
  customerId: "acme-inc", // <- attributes the spend; stripped before the real call
});

wrapAnthropic is fire-and-forget: tracking never blocks or fails your LLM call. Events are buffered and flushed automatically (see Batching below). Pass { onError } to observe enqueue-time failures. Streaming calls (stream: true) are passed through untracked for now.

Quick start — auto-instrument OpenAI

import { TokenOps, wrapOpenAI } from "@lovieco/tokenops";
import OpenAI from "openai";

const tokenops = new TokenOps({
  companyId: "<your-company-uuid>",
  secretKey: process.env.TOKENOPS_SECRET_KEY!, // sk_live_...
  ingestUrl: "https://api.lovie.co",
});

// One line. Every non-streaming call is now attributed.
const openai = wrapOpenAI(new OpenAI(), tokenops);

await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "hi" }],
  customerId: "acme-inc", // <- attributes the spend; stripped before the real call
});

responses.create is instrumented the same way when your OpenAI SDK exposes it — older SDKs without it are left untouched. Like wrapAnthropic, wrapOpenAI is fire-and-forget: tracking never blocks or fails your LLM call, events are buffered and flushed automatically (see Batching below), and { onError } observes enqueue-time failures. Streaming calls (stream: true) are passed through untracked for now.

Track events directly

For other providers or custom flows, enqueue events yourself:

tokenops.track({
  vendor: "openai",
  model: "gpt-4o",
  inputTokens: 1200,
  outputTokens: 350,
  customerId: "acme-inc",
  providerObservationId: "resp_123", // idempotent de-dup on re-send
});

track() buffers the event and returns immediately. If you omit providerObservationId, the SDK sends the event's generated id as the observation id so the ingest contract is always satisfied.

Batching, flushing, and shutdown

track() buffers events and flushes them in the background — when the buffer reaches batchSize, and on a flushIntervalMs timer. This coalesces many calls into few HTTP requests.

Because buffered events are sent asynchronously, a process that exits before the next flush would drop them. In short-lived or serverless code, drain explicitly:

await tokenops.flush(); // send everything buffered right now; rejects on failure
await tokenops.close(); // stop the timer and flush; call once on shutdown

Background flush failures do not throw into your app — configure onError to observe them.

Send a batch synchronously

trackBatch() sends immediately and resolves once the request succeeds (or rejects after retries), bypassing the buffer. Requests larger than 500 events are split automatically.

await tokenops.trackBatch([event1, event2]); // up to 500 per request

Failed sends retry with exponential backoff on network errors, 429, and 5xx; a 4xx (e.g. a bad key) throws a TokenOpsError immediately.

Configuration

| Option | Default | Description | | ----------------- | ---------- | -------------------------------------------------------- | | companyId | (required) | Your Lovie company UUID. | | secretKey | (required) | A TokenOps secret key (sk_live_...). Keep server-side. | | ingestUrl | (required) | Base URL of the ingest endpoint. | | batchSize | 20 | Buffered events that trigger an automatic flush. | | flushIntervalMs | 5000 | Background flush interval. 0 disables the timer. | | maxRetries | 3 | Retry attempts on network / 429 / 5xx. | | timeoutMs | 10000 | Per-request timeout. | | onError | — | Called when a background flush fails. | | fetch | global | Injectable fetch implementation. |

The raw contract

Under the hood each request POSTs Connect-JSON to {ingestUrl}/lovie.tokenops.v1.IngestService/IngestSdkEvents:

{
  "secretKey": "sk_live_...",
  "events": [
    {
      "id": { "value": "<uuid>" },
      "companyId": { "value": "<company-uuid>" },
      "vendor": "anthropic",
      "lineType": "inference",
      "model": "claude-sonnet-4-5",
      "inputTokens": 1200,
      "outputTokens": 350,
      "customerId": "acme-inc",
      "startedAt": "2026-07-21T00:00:00Z",
      "endedAt": "2026-07-21T00:00:01Z",
      "providerObservationId": "resp_123"
    }
  ]
}

id and companyId are UUID wrappers ({ "value": "<uuid>" }), and providerObservationId is required. You can send the same request with curl — see your TokenOps dashboard's integration guide for a copy-paste snippet.

License

MIT