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

valora-sdk

v0.1.1

Published

Valora Node SDK for instrumenting agent executions.

Readme

valora-sdk

Know what your AI agents cost, and who to bill for it.

Wrap a function; Valora records every run — how long it took, whether it succeeded, which customer it was for, and what the LLM calls inside it cost.

import { track } from 'valora-sdk'

export const summarise = track(async (doc: string) => {
  const reply = await openai.chat.completions.create({ /* ... */ })
  return reply.choices[0].message.content
}, { agent: 'summariser' })

That is the whole integration. Token counts and costs are picked up automatically — you do not pass them in.

  • Node 20+, zero runtime dependencies
  • Server-side only (why)
  • It cannot break your app — see the guarantees

Install

npm install valora-sdk

Node 20 or later. No runtime dependencies — HTTP goes through the global fetch, and nothing else is pulled into your tree.

Installing from a local checkout instead? Use a packed tarball — npm pack here, then npm install <the-tarball> in your app — and not file:../valora-sdk-node. A file: dependency is a symlink, and Node resolves symlinks to their real path, which stops the SDK finding your openai package and silently disables cost capture.

Configure

Two environment variables. The SDK reads them once, on first use — it takes no arguments, so a key never ends up in your source or your version control.

VALORA_API_KEY=vk_...            # from your Valora dashboard
VALORA_BASE_URL=https://app.valora.technology

If either is missing, the first tracked call throws a ConfigurationError saying which. That is deliberate: an SDK that silently records nothing is worse than one that tells you on your first run.

Track your first agent

track() wraps a function and gives you back one with the same signature:

import { track } from 'valora-sdk'

const research = track(async (topic: string) => {
  // ... whatever your agent does
  return findings
}, { agent: 'research-bot' })

await research('quarterly filings')   // recorded

The wrapped function is always async, even around a synchronous one — the SDK verifies your credentials before your code runs, and Node has no blocking HTTP. If you wrap a synchronous function, its callers now need to await it.

The agent alias is an identity

agent: 'research-bot' names the agent. The SDK creates it on first use and derives a stable id from it.

Changing that string forks the agent's history — the new name becomes a new agent and the old one stops receiving runs. Pick it once. If it happens anyway, the fix is merging the two, not renaming back.

Attribute a run to a customer

Executions carry a client so you can bill per customer. There are two ways, and you should use one of them per function.

A selector, when the customer id is in the arguments:

const chargeCustomer = track(async (order: Order) => {
  // ...
}, {
  agent: 'billing-bot',
  client: ([order]) => order.customerId,      // type-checked
})

The selector receives the call's arguments as an array, and is type-checked against your function — so a typo is a build error rather than a silent null.

setClient(), when the id is only known once you are inside:

import { track, setClient } from 'valora-sdk'

const runBilling = track(async (payload: Payload) => {
  const tenant = await resolveTenant(payload)
  setClient(tenant.id)
  // ...
}, { agent: 'billing-bot' })

Pass a string, number, or something with a real toString() — an id, not the object it came from. Passing a whole database row is the easy mistake: its toString() is "[object Object]", which would file every customer under one name. The SDK rejects that and tells you rather than inventing a client.

If you use both, the selector wins and the SDK warns. A run whose client cannot be resolved is still recorded — you never lose the execution over it.

Give your customers real names

You do not have to register anything. The id above is enough: the SDK creates the client the first time it sees one, so no usage is ever lost or waits on setup.

What you get without registering is a client named after its raw id — cus_9f2a11, not Acme Corp. Registration exists to fix that, and there are two entry points:

import { registerClient, registerClients } from 'valora-sdk'

// In your own signup flow, when you first have the company's real name:
await registerClient(tenant.id, tenant.companyName)

// Once, when you onboard, for the customers you already have:
const summary = await registerClients([
  ['cus_9f2a11', 'Acme Corp'],
  ['cus_4b7c03', 'Globex'],
])
// → { created: 2, alreadyExisted: 0, failures: [] }

Do it before that customer's first tracked run. This is the one thing worth knowing: Valora keeps the name a client was created with, so registering a customer you have already billed for does not rename them. It is not an error and nothing is lost — the name simply stays as it was, and changing it becomes a job for the dashboard.

The two behave differently on failure, on purpose:

  • registerClient never throws. It runs in your signup path, and Valora being unreachable must not fail a customer's signup. The cost of a swallowed failure is the display name, nothing more.
  • registerClients throws on a bad credential and reports per-row failures in its summary. It is setup-time work you are watching, and a backfill that printed "done" having written nothing would be worse than one that stopped.

registerClients takes anything iterable of [id, name] — an array, a Map, or Object.entries(yourObject) — paces itself between requests, and is safe to re-run: it creates no duplicates and changes no names, so a run interrupted half-way can just be started again.

LLM cost capture

If the openai package is installed, the SDK patches it on your first tracked call and attaches token counts automatically. There is nothing to configure and nothing to import — and openai is never a dependency of this package, so nothing changes if you do not use it.

This covers more than OpenAI itself:

  • OpenAI-compatible providers — Moonshot, DeepSeek, Groq, Together, Azure — used through the same client with a different baseURL.
  • The Vercel AI Gateway, recorded with channel: "vercel" so gateway spend is distinguishable from direct spend.
  • LangChain, whose ChatOpenAI calls out through this same openai package.

Both chat.completions.create and embeddings.create are captured. Your call's return value is handed back exactly as the vendor produced it — the SDK observes the result, it does not wrap or replace it, so withResponse() and asResponse() keep working.

Streamed responses need one flag

Streamed calls are captured too — but only if you ask OpenAI to send the counts, because a stream carries no usage unless you do:

const stream = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages,
  stream: true,
  stream_options: { include_usage: true },   // ← without this there are no counts
})

The SDK does not add the flag for you. Injecting it would mean rewriting your request, and a usage chunk your own code did not expect can break a consumer that assumes every chunk has a choices[0].

Without it the execution is still recorded — it simply carries no token counts, and the SDK warns once telling you so.

Your stream is not buffered, copied, or delayed. The SDK reads each chunk as it passes on its way to you and never pulls ahead, so a stream you abandon part-way stops at the network exactly where it would have without us. The object you receive is the vendor's own Stream, with tee() and toReadableStream() intact.

It also does not matter where you consume it. Read the stream inside your tracked function and the counts ride out on the same request as everything else; return it and read it afterwards — from a route handler, say — and they follow shortly after, attached to the execution they belong to.

Before your process exits

Does your platform stop the process right after a response? If so, read this. If you run a long-lived server — Express, Fastify, a container, a VM — the background drain covers you and there is nothing to do.

The SDK drains what it has buffered when a process ends naturally. Two common situations skip that:

| | | | --- | --- | | process.exit() | Node runs no async work after it. Anything queued is lost. | | serverless — Vercel, AWS Lambda, Cloudflare Workers, Netlify, Cloud Functions | the sandbox can be frozen the instant your handler returns |

This has nothing to do with which model provider you call. A handler using OpenAI directly on Lambda is affected; one using Vercel AI Gateway on a long-running server is not. What matters is where your code runs.

The fix is one line, and it works everywhere:

import { track, flush } from 'valora-sdk'

export async function POST(request: Request) {
  const result = await tracked(request)
  await flush()               // waits for delivery; adds our request to yours
  return Response.json(result)
}

If your platform can keep the sandbox alive after the response, prefer that — it costs your users no latency:

import { after } from 'next/server'          // Next.js 15.1+
after(() => flush())

// or, on other Vercel runtimes:
import { waitUntil } from '@vercel/functions'
waitUntil(flush())

// or, on Cloudflare Workers:
ctx.waitUntil(flush())

If you forget, the SDK says so rather than losing your data quietly:

valora-sdk: exiting with 4 execution(s) still queued. They were NOT delivered.
  Call `await flush()` before your process ends — see the 'Before your process
  exits' section of the README.

What it will never do to your app

These are guarantees, not intentions — each is enforced by tests.

It will not break your code. Everything the SDK does sits inside a boundary that catches its own failures. If Valora is unreachable, if we send something the API rejects, if our own code has a bug — your function still runs and still returns its value. The one deliberate exception is a missing or invalid API key, which throws on your first call so you find out immediately rather than after a week of recording nothing.

It will not slow you down. An execution is assembled in memory and delivered off your critical path, from a small bounded queue drained by one worker. Your tracked function returns without waiting for us.

It will not hold your process open. The queue's timers are unref'd, so Node exits when your work is done, not ours.

It will not grow without bound. Under a sustained outage the queue fills at 1000 executions and drops the oldest, counted and logged. Blocking your app instead would put our latency on your critical path; buffering forever could exhaust your memory.

Your errors stay yours. An exception your function throws propagates untouched — the SDK records the run as failed and rethrows the original.

Node only, never the browser

Two independent reasons, both hard:

  1. The SDK holds an API token. Bundling it into browser code publishes that token to anyone who opens devtools.
  2. Context propagation is built on node:async_hooks, which has no browser equivalent.

There is no browser build and there will not be one.

What gets sent to Valora

Per execution: a client-generated id, the agent, start and end timestamps, the status, the client id where you supplied one, and token counts from LLM calls made inside it.

Not your arguments, not your return values, not your prompts or completions. The SDK never reads them.

If the SDK silences one of its own errors, it reports that separately so the failure is not invisible. Those reports carry the error's type and which internal boundary failed — never a stack trace, file path or line of your source. Message text is included only for the SDK's own error types, because we author those strings; a foreign error's message is blanked, since it routinely embeds the value that caused it.

Troubleshooting

No executions appear. Check VALORA_API_KEY and VALORA_BASE_URL, then look for valora-sdk: lines in your logs — every warning carries that prefix. If your process is short-lived or serverless, see above.

Executions appear but have no cost data. Either the openai package is not resolvable from where the SDK is installed, or you are streaming without stream_options: { include_usage: true }.

Rows have no customer. A client value the SDK could not use is warned about at the call site — look for valora-sdk: ignored a client value….

Errors this SDK can throw

All extend ValoraError, so one catch covers them:

| | | | --- | --- | | ConfigurationError | a missing or unusable VALORA_API_KEY / VALORA_BASE_URL | | AuthenticationError | the key was rejected | | TransportError | Valora was unreachable, or answered 5xx | | ResolutionError | an agent or client could not be created | | ExecutionError | an execution was rejected |

In practice only ConfigurationError and AuthenticationError reach your code; the rest happen off your critical path and are handled internally.

Coming from the Python SDK?

valora_sdk (Python) is the reference implementation and this is a port, not a fork: the wire payload and the derived ids are identical by contract, so the same agent alias used from both languages resolves to the same agent rather than forking into two billing histories.

One deliberate difference. Python's @track(client='customer') names a function parameter and reads it by reflection. TypeScript cannot do that — and parameter names are erased by minification in any production build, which would make attribution fail silently in exactly the deployments this SDK targets. Node uses a type-checked selector instead.

Everything else you know carries over. register_client / register_clients are registerClient / registerClients, with the same semantics and the same split between the forgiving signup path and the strict backfill; the backfill takes an iterable of pairs here too, and returns a summary rather than raising.

The two packages have separate version numbers and always will. They are separate implementations of one contract, and neither is a superset of the other — so a matching number would promise a parity that does not exist. Do not read anything into [email protected] for Node sitting next to valora-sdk==0.1.1 for Python.

What is guaranteed across them is the part that would actually hurt to get wrong: the wire payload and the derived agent and client ids are identical by contract, pinned by shared test vectors. The same alias used from both languages resolves to the same agent, on any pair of versions.

Development

npm ci
npm run build     # dual ESM + CJS into dist/
npm test          # type-checks and runs the suite on the source
npm run smoke     # loads the built artifact both ways, as a customer would

npm test and npm run smoke check different things and neither subsumes the other — see the comment at the top of scripts/smoke.mjs.