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

keeperhub-tab

v0.1.12

Published

Spend control for onchain agents. Bounds who an agent pays and how much, and writes every decision to a record anyone can check.

Readme

keeperhub-tab

Spend control for KeeperHub agentic wallets. A drop-in PreToolUse hook that governs every payment your agent makes — a counterparty allowlist and a rolling budget, checked before anything signs — and hash-chains every decision so the record can be replayed by anyone.

KeeperHub's wallet checks each payment in isolation. keeperhub-tab adds which counterparties this agent may pay, how much in total across a rolling window, and a record of every decision.

🔎 Live verifier · 📖 Docs · 📐 How it works · 💻 GitHub


Install

npm i keeperhub-tab @keeperhub/wallet

@keeperhub/wallet is a peer dependency — Tab wraps the wallet you already have. Tab itself has zero runtime dependencies.

Quickstart — two lines

import { createTab } from 'keeperhub-tab';

const tab = await createTab({
  agent: walletAddress,
  counterparty: { allow: ['0x…the-service-you-buy-from'] },
  budget: { capUsd: 5 },
});

export default tab.hook;   // drop-in for KeeperHub's PreToolUse hook

Every payment now passes the allowlist and the budget before it signs, and every decision — KeeperHub's and Tab's — lands in a hash chain.

agent tries to pay
        │
        ▼
  ┌──────────────────────────────────────────────────┐
  │  KeeperHub hook   → amount tiers · token allowlist│
  │                     a deny here is FINAL          │
  ├──────────────────────────────────────────────────┤
  │  tab.hook                                         │
  │   1. counterparty  → not vetted?     ✗ deny       │
  │   2. budget        → over the cap?   ✗ deny       │
  └──────────────────────────────────────────────────┘
        │                          │
   ✓ allowed                  every decision
        ▼                          ▼
   payment signs           🔗 hash chain → Tab.sol

Tab may only tighten. KeeperHub's hook is consulted first and its denial is final; no Tab policy can turn a deny into an allow. That is what makes it safe to drop into an agent that already works.

createTab(opts)

The one-call setup. Composes KeeperHub's hook with Tab's policies, defaults the recorder, and returns a hook you can export directly.

What you pass:

| Option | Type | Default | | | --- | --- | --- | --- | | agent | string | required | Identifier recorded against every decision — usually the wallet address | | counterparty | CounterpartyOptions | skipped | { allow: string[], onUnknown?: 'deny' \| 'ask' } — who the agent may pay | | budget | BudgetOptions | skipped | { capUsd: number, windowMs?: number, store?: SpendStore } — cumulative cap | | policies | Policy[] | [] | Extra policies, evaluated after the built-ins in array order | | recorder | Recorder | in-memory | Where decisions are written. Use FileRecorder to survive a crash |

Omit counterparty or budget and that check is skipped entirely — there is no silent default cap.

What you get back:

const { hook, recorder, policies } = tab;
//      └ export it   └ read the chain   └ what's active, in order

Defaults worth knowing: windowMs is 24h, onUnknown is 'deny' (a challenge with no readable recipient is refused, not guessed), and the built-ins always run counterparty before budget, so a payment to a stranger never consumes budget headroom.

Set it up from the terminal (no code)

Registers the hook in ~/.claude/settings.json and writes your policy to ~/.tab/config.json:

npx keeperhub-tab init 0x…the-service-your-agent-pays --cap=5
npx keeperhub-tab allow 0x…another-service
npx keeperhub-tab status

The allowlist starts empty on purpose — a fresh install refuses every payment and tells you why, because a control that permits everything until configured is not a control.

Registration is surgical: it preserves every unrelated key and every sibling command already in your PreToolUse block.

Decisions

Every call returns KeeperHub's decision shape, so it is a true drop-in:

type Decision =
  | { decision: 'allow' }
  | { decision: 'ask';  reason?: string }
  | { decision: 'deny'; reason: string };

| Reason | Meaning | | --- | --- | | COUNTERPARTY_NOT_ALLOWLISTED | Recipient is not in allow | | BUDGET_EXCEEDED | Payment would cross capUsd inside the window | | RECIPIENT_UNDETERMINED | The challenge carried no readable recipient | | AMOUNT_UNDETERMINED · AMOUNT_MALFORMED | The amount could not be read, or is not a valid non-negative integer | | CHALLENGE_UNEVALUABLE | A 402 the signer seam could not parse — refused rather than paid blind | | BLOCKED_BY_SAFETY_RULE · ASK_REQUIRED | Came from KeeperHub's own hook, passed through unchanged |

Unknowns fail closed. An amount or recipient Tab cannot read is refused, never defaulted.

The second seam — createGovernedSigner

The PreToolUse hook governs agent frameworks. A backend service, a cron job, or a non-Claude runtime that imports the package and calls paymentSigner.fetch() directly never fires a hook at all. createGovernedSigner applies the same policy at that seam:

import { createTab, createGovernedSigner, PaymentRefused } from 'keeperhub-tab';

const tab = await createTab({ agent, counterparty: { allow }, budget: { capUsd: 5 } });
const signer = createGovernedSigner({ hook: tab.hook });

try {
  const res = await signer.fetch('https://api.example.com/paid-endpoint');
} catch (e) {
  if (e instanceof PaymentRefused) console.error(e.reason, e.url);
}

It reads the 402 challenge, rules on it, and pays that same response rather than re-fetching the URL. A server that quoted $0.01 to the check and $90 to the payment would otherwise pass a check that never saw the second quote.

Durable and cross-process storage

The defaults are in-memory, which is right for a single short-lived process and wrong for anything else.

import { FileRecorder, FileSpendStore } from 'keeperhub-tab';

const recorder = new FileRecorder('./.tab/decisions.ndjson');  // append-only, fsync per write
const store    = new FileSpendStore('./.tab/spend');           // atomic across processes

const tab = await createTab({
  agent,
  counterparty: { allow: ['0x…'] },
  budget: { capUsd: 5, store },
  recorder,
});

FileRecorder tolerates a torn final line from a crash mid-write, and refuses to load a log corrupted in the middle rather than silently continuing from a broken chain. FileSpendStore guards the ledger with an exclusive-create lock, so concurrent agent processes on one filesystem cannot both slip under the same cap.

For agents spread across machines, implement SpendStore against Redis or Postgres — nothing else changes:

interface SpendStore {
  /** True if `amount` fit within the cap and was committed. Must be atomic. */
  reserve(agent: string, amount: bigint, windowMs: number, capMicro: bigint): Promise<boolean>;
  /** Total committed within the window. */
  spent(agent: string, windowMs: number): Promise<bigint>;
  /** Give headroom back when a later policy denies. */
  release(agent: string, amount: bigint): Promise<void>;
}

The reserve is provisional until the whole evaluation ends in an allow, so requests that are refused later — or that KeeperHub turns into an ask — return their headroom. Without that, anyone able to trigger refused payments could exhaust your budget without ever spending a cent.

Anchor the record on-chain

AutoAnchor commits the chain head on a cadence, so a compromised operator cannot quietly rewrite history:

import { AutoAnchor } from 'keeperhub-tab';

new AutoAnchor(recorder, sink, { everyDecisions: 50, everyMs: 300_000 }).start();

Both triggers matter: a count threshold bounds the window during a busy run, a time threshold bounds it during a quiet one. Supply any sink that can write a head:

interface AnchorSink {
  anchor(head: string, count: number): Promise<{ txHash: string }>;
}

One transaction attests to every decision since the last anchor — the hash chain is what makes that batching safe. A reference sink using KeeperHub's DirectExecutor (which signs and sponsors the gas) is in the repository.

Exports

| | | | --- | --- | | createTab(opts) | One-call setup — policies, recorder, hook | | createGovernedSigner(opts), PaymentRefused | The library seam, for callers with no framework hook | | CounterpartyPolicy, BudgetPolicy | The individual policies, composable on their own | | ChainedRecorder, ConsoleRecorder, FileRecorder, rewriteLog | Recorders, and the tamper helper the verifier demo uses | | MemorySpendStore, FileSpendStore | Spend ledgers | | AutoAnchor | Scheduled on-chain commitment | | hashRecord, canonical, GENESIS | The hash chain, so you can verify a log yourself | | extractIntent, usdToMicro, formatUsd | Challenge parsing and unit conversion | | registerHook, loadConfig, saveConfig, configPath, DEFAULT_CONFIG, runCli | What the CLI is built from | | Decision, Policy, Recorder, SpendStore, TabRecord, PaymentIntent, AnchorSink, … | Types |

Every export documented with parameters, types and examples → tab-verify.vercel.app/docs

Requirements

  • Node 20+. Ships ESM and CommonJS — import and require both work, with types resolving under each condition.
  • @keeperhub/wallet as a peer. createTab calls its createPreToolUseHook() unless you inject your own.
  • Amounts are handled as bigint micro-USDC end to end, so a value past Number.MAX_SAFE_INTEGER is compared exactly rather than rounded.

License

MIT