Skip to content

Hosted audit logging

built by productdevbook

who did what, when, and what it cost.

NitroPing is an audit log for TypeScript services. Record an event with one call, get an immutable row you can query, and read it back in a panel built for the question you actually have.

read the docs

The second one copies a prompt that points Claude Code, Cursor or Codex at llms.txt — the whole integration in one file. It does the wiring.

live/audit_events6 events
timestampactor_idaction
09:12:04.118u_8f31c2user.login
09:12:04.507u_8f31c2api_key.create
09:12:06.221svc_billinginvoice.void
09:12:07.883openaiopenai.chat
09:12:09.010anonymoushttp.request
09:12:11.442u_66d1eemember.remove
Every row is server-stamped. id, tenant_id, ip and timestamp are never taken from the caller.

Three steps

From nothing to a queryable trail.

No agent to run, no schema to design, no table to migrate. The SDK batches in the background and flushes on exit, so a serverless invocation does not drop its last events.

  1. 01

    Install

    One package. Adapters for Hono, Express and Nitro ship as subpath exports.

    terminal
    $ bun add nitroping
  2. 02

    Record

    track() returns immediately — it queues. A slow endpoint never slows the request you are auditing.

    app.ts
    import { NitroPing } from "nitroping"
    
    const audit = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
    
    audit.track({
      actor_id: user.id,
      actor_type: "user",
      action: "invoice.void",
      target_type: "invoice",
      target_id: invoice.id,
      severity: "warning",
      metadata: { amount_cents: 42_900, reason: "duplicate" },
    })
  3. 03

    Read it back

    Filter, group and follow a session in the panel — or let an agent do it: bunx nitroping-mcp logs itself in, no key to paste, and can mark what it finds resolved.

    mcp tools
    • project_health
    • error_groups
    • find_errors
    • endpoint_health
    • actor_summary
    • list_events
    • resolve_error
    • list_error_states

For agents

Hand it to the thing that writes your code.

The whole integration is one file, written to be read by a model: the event shape, the four fields the server owns, the middleware, the GenAI wrappers, and the rules for not filling an audit log with noise. Paste this and let the agent do the wiring.

paste into your agent

Read https://nitroping.dev/llms.txt and add NitroPing audit logging to this codebase. Use the framework middleware for requests, and record the decisions that matter — sign-in, permission changes, money, deletion. Read the key from NITROPING_KEY; never commit it or ship it to the browser.

Everything in one fetch

Install, key, event shape with every length and enum, middleware, wrappers, raw HTTP and OTLP. No page to crawl, no docs site to guess at.

Taste, not just syntax

It says what to audit and what to leave alone — a row per debug log is noise that costs money — so the agent writes a log you can still read in a year.

Then it can read it back

bunx nitroping-mcp logs itself in — no key to paste — so the agent that wired the events up can go ask what they say, and mark what it fixes resolved.

Integrity

Four fields the caller does not get to choose.

An audit log is only worth keeping if the parts that matter cannot be written by the thing being audited. These are stamped at the endpoint, after the request is authenticated and before the row exists.

idserver-assigned

UUIDv7, assigned on write.

Time-ordered, so a row's position in the log is a fact about when it happened, not about when it arrived.

tenant_idserver-assigned

Taken from the API key.

A client that posts its own tenant_id is overwritten with the key's. There is no way to write into someone else's log.

ipserver-assigned

Read from the connection.

Never accepted from the body. Send X-NitroPing-No-IP and the row is written without it rather than blanked afterwards.

timestampserver-assigned

Normalised to UTC milliseconds.

One conversion, in one place. A client sending +03:00 does not land three hours out.

GenAI

The same log, priced.

Model calls are actions with an actor, a target and a cost. They belong in the audit trail rather than in a second tool — so the wrappers write to the same table, and the cost lands on the row.

one line at construction
import { NitroPing } from "nitroping"

const audit = new NitroPing({ apiKey: process.env.NITROPING_KEY! })
const openai = audit.observeOpenAI(new OpenAI())

// Nothing else changes. Every call now writes a row with
// input_tokens, output_tokens, cached_input_tokens and cost_usd.
await openai.chat.completions.create({ model: "gpt-4o-mini", messages })

Eighteen providers

OpenAI, Anthropic, Google, Bedrock, OpenRouter and LiteLLM get a drop-in wrapper that returns the client you passed it. The Vercel AI SDK, TanStack AI and Eve hook in through onFinish.

List prices built in

Cached input and reasoning tokens are priced separately, because billing prices them separately. Override the table when your contract is not list price.

Prompts stay out by default

Token counts and cost are recorded; prompt and completion text is not, unless you set captureContent. It is the field most likely to carry personal data.

Agent runs nest

Tool calls hang off the run that made them, so a trace waterfall shows where an agent spent its time and its budget.

Integration

Or send nothing at all.

Middleware turns every request into an http.request event with method, path, status and latency. If your service already speaks OpenTelemetry, point the exporter here and skip the SDK.

Framework middleware

hono · express · nitro
import { nitropingMiddleware } from "nitroping/hono"

app.use(nitropingMiddleware({
  apiKey: process.env.NITROPING_KEY!,
  getActor: (c) => c.get("user"),
}))

A thrown exception is captured as both the request's error and a separate error.captured event with the stack, then rethrown. Your own error handling is untouched.

OpenTelemetry

.env
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $NITROPING_KEY"

Traces and metrics are converted to audit events on arrival, over OTLP's HTTP transport — both http/protobuf and http/json, never gRPC. A few tools have to be told which, and that is the only extra line any of them need. Per-tool setup.

Control

It is your log.

Two things a hosted audit log has to answer for: getting a person out of it, and not being the only place it can run.

Erasure by actor or address

Delete every row for one actor_id or one ip, optionally only before a date. Deleting a project schedules the same erasure for its events — they do not outlive the record that named them.

Run the API yourself

The ingest and control-plane API is a single Rust binary with Postgres behind it. Migrations run at boot, so the schema and the binary that depends on it ship together. The SDK takes an endpoint and points anywhere.

Start writing rows in about a minute.

Sign in with GitHub, create a key, send an event. The panel shows it arriving.