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

@katrinalaszlo/agentkey

v0.2.0

Published

Scoped, budgeted, time-bounded API keys for AI agents

Readme

agentkey

Scoped, budgeted, time-bounded API keys for AI agents.

Why

When I led self-serve at a usage-based data company, one of the most common requests was credit limits per API key. Users have asked for the same at Tanso. Account- and user-level limits are critical for enterprises, but they're heavy. What's the simple version for startups?

agentkey is that: cap what a key can spend, scope what it can do, set when it expires, and record which human authorized it. It adds a few columns to your existing keys table — it doesn't replace your auth.

Scoped keys control what an agent can do, not how much it can spend. agentkey does both, per key. LLM gateways cap spend; identity platforms scope keys; neither does both at the key level.

What it covers

The layers nobody else covers, per key:

| Layer | What it controls | Who covers it today | |---|---|---| | Identity | Who is this | Clerk, Auth0 | | Account billing | How much can this org spend | Stripe, Metronome | | Key scoping | What can this key do | agentkey | | Key budgeting | How much can this key spend | agentkey | | Key expiry | When does access end | agentkey | | Delegation | On whose behalf | agentkey |

Install

npm install @katrinalaszlo/agentkey

Quick Start

import { AgentKey } from '@katrinalaszlo/agentkey';

const ak = new AgentKey({ pool }); // pass your pg Pool

// Create a scoped key with a budget
const key = await ak.create({
  accountId: 'acct_123',
  scopes: ['usage.read', 'proxy.chat'],
  budgetCents: 5000,        // $50 cap
  budgetPeriod: 'month',
  expiresIn: '7d',
  delegatedBy: 'user_456',  // human who authorized this agent
  name: 'sales-agent',
});
// => { key: 'ak_7f3a...', id: 42, expiresAt: '2026-05-20T...' }

// Validate on every request
const result = await ak.validate(key.key);
// => { valid: true, scopes: ['usage.read', 'proxy.chat'],
//      budgetCents: 5000, budgetUsedCents: 1200,
//      budgetRemainingCents: 3800, expiresAt: '...',
//      delegatedBy: 'user_456', accountId: 'acct_123' }

// Track spend after an LLM call
await ak.trackUsage(key.key, { costCents: 15 });

// Check if a scope is allowed
ak.hasScope(result, 'proxy.chat');  // true
ak.hasScope(result, 'billing.write'); // false

How It Works

agentkey adds columns to your existing API keys table and provides middleware to enforce scopes and budgets on every request.

One account, multiple keys, different capabilities:

Account: Acme Corp (Pro plan, $100/month)
  |
  |-- ak_sales_...    scopes: [proxy.chat]         budget: $40/mo
  |-- ak_analytics_.. scopes: [usage.read]          budget: $0 (free endpoints only)
  |-- ak_agent_...    scopes: [proxy.chat, usage.read]  budget: $30/mo  expires: 7d

The account's plan sets the ceiling. Keys subdivide it. No single key can blow the whole month's budget.

API

new AgentKey(options)

const ak = new AgentKey({
  pool,              // pg Pool instance
  tableName: 'sdk_api_keys',  // default
  keyPrefix: 'ak_',           // default
});

ak.create(options)

Create a new scoped key.

| Option | Type | Required | Description | |---|---|---|---| | accountId | string/number | yes | Account this key belongs to | | scopes | string[] | no | Allowed actions. null = unlimited | | budgetCents | number | no | Spending cap in cents. null = unlimited | | budgetPeriod | 'day' | 'month' | null | no | Budget reset interval | | expiresIn | string | no | Duration: m=minutes, h=hours, d=days, mo=months (e.g. '30m', '1h', '7d', '1mo'). null = no expiry | | delegatedBy | string | no | User ID of the human who authorized this key | | name | string | no | Label for this key |

ak.validate(rawKey)

Validate a key and return its metadata. Returns { valid: false, reason: string } for invalid, expired, or revoked keys.

ak.trackUsage(rawKey, { costCents })

Increment budget usage. Returns { success: false, reason: 'budget_exceeded' } if the key's budget cap would be exceeded.

ak.hasScope(validationResult, scope)

Check if a validated key has a specific scope. Returns boolean.

ak.revoke(keyId, accountId?)

Soft-revoke a key (sets revoked_at timestamp). Returns true if a key was revoked, false if nothing matched. Pass accountId to only revoke a key that account owns — the built-in DELETE /sdk-keys/:id route does this so one key can't revoke another account's keys by guessing IDs.

External-subject keys (ak.ensureSubject / ak.validateBySubject / ak.trackUsageBySubject)

For agents that already carry a credential from an identity provider (e.g. a Clerk M2M token), you can anchor a budget row to that external identity instead of minting an ak_ key. Same budget/scope/expiry enforcement, keyed on the external subject.

  • ak.ensureSubject(subject, options?) — create-on-first-seen a budget row for an external identity. Idempotent (a second call is a no-op). options takes the same scopes/budgetCents/budgetPeriod/expiresIn/delegatedBy/name as create, plus optional accountId (defaults to the subject). Returns nothing — no token is issued; the external credential is the bearer.
  • ak.validateBySubject(subject) — same as validate, keyed on the external subject.
  • ak.trackUsageBySubject(subject, { costCents }) — same as trackUsage, keyed on the external subject.

These power @katrinalaszlo/agentkey-clerk. Requires running ak.migrate() (adds the external_subject column).

Express Middleware

import { agentKeyMiddleware } from '@katrinalaszlo/agentkey';

// Protect routes with scope checks
app.get('/api/usage', agentKeyMiddleware(ak, { scope: 'usage.read' }), handler);
app.post('/api/proxy', agentKeyMiddleware(ak, { scope: 'proxy.chat' }), handler);

// Budget is tracked automatically when you call ak.trackUsage()

Self-Serve Routes (optional)

createAgentKeyRoutes(ak, opts) mounts POST /signup, GET /sdk-keys/me, POST /sdk-keys, and DELETE /sdk-keys/:id.

import { createAgentKeyRoutes } from '@katrinalaszlo/agentkey';

app.use(createAgentKeyRoutes(ak, { signupScopes: ['proxy.chat'] }));

Security model:

  • POST /signup is unauthenticated (an agent self-serves a key with just an email). It only grants scopes listed in signupScopes. With signupScopes unset it issues a scopeless key — it never passes caller-supplied scopes (or an unlimited scope) through, so no one can mint an admin key from this endpoint. Set your own budget_cents/expires_in caps in front of it if you expose it publicly.
  • POST /sdk-keys requires a valid key and attenuates to the caller: it can only grant scopes the calling key already holds, and a child key's budget and expiry cannot exceed the calling key's.
  • DELETE /sdk-keys/:id only revokes keys owned by the calling key's account.

Database Migration

agentkey adds columns to your existing keys table:

ALTER TABLE sdk_api_keys
  ADD COLUMN IF NOT EXISTS user_id TEXT,
  ADD COLUMN IF NOT EXISTS scopes TEXT[],
  ADD COLUMN IF NOT EXISTS budget_cents INTEGER,
  ADD COLUMN IF NOT EXISTS budget_used_cents INTEGER DEFAULT 0,
  ADD COLUMN IF NOT EXISTS budget_period TEXT,
  ADD COLUMN IF NOT EXISTS budget_reset_at TIMESTAMPTZ,
  ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ,
  ADD COLUMN IF NOT EXISTS delegated_by TEXT;

Run ak.migrate() to apply automatically, or use the SQL above in your own migration system.

Use with Clerk

If your agents authenticate with Clerk M2M tokens, you don't need to mint a separate ak_ key. @katrinalaszlo/agentkey-clerk is a drop-in middleware that verifies the Clerk token and enforces an agentkey budget/scope/expiry on the machine behind it — Clerk says which machine is calling, agentkey says how much it can spend. The agent keeps carrying its Clerk token; the spend layer rides on top.

The subject-keyed methods that power it (ensureSubject, validateBySubject, trackUsageBySubject) are part of agentkey's API and can be used directly against any external identity, not just Clerk.

Why Not Just Use...

Clerk/Auth0: They scope identity, not budget. M2M tokens have scopes but no credit caps, no usage metering per key.

Stripe/Metronome: They scope account billing, not per-key. Can't tell which of 15 keys drove the cost.

Rate limiters: They scope throughput (requests/min), not dollars. 10 requests at $50 each stays under the rate limit while spending $500.

Custom code: This is what everyone builds. It takes weeks, it's different at every company, it has bugs.

License

MIT