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

@vaduno/agent

v0.7.1

Published

Spend-firewall hooks for AI agent frameworks. Binds Vaduno policy to decide-only tool-approval hooks (Claude Agent SDK PreToolUse and friends) via authorize/settle. Never holds funds or keys to funds.

Readme

@vaduno/agent

Spend-firewall hooks for AI agent frameworks. Binds a Vaduno policy to a framework's tool-approval hook, so a model that decides to spend money still has to get past a cap it does not control.

Part of Vaduno — non-custodial by construction. This package never holds funds, never touches keys to funds, and never moves money. It answers allow/deny and records what happened.

npm install @vaduno/agent @vaduno/guard

Why this package exists

guard.execute(intent, executor) requires the guard to own the payment call. No agent framework's approval hook works that way — every one of them is decide-only: it hands you a pending tool call, takes an allow/deny, and runs the tool itself.

So this binds to the two-phase path instead:

before the tool runs → authorize() → allow or deny
after the tool runs  → settle()    → executed or failed

An authorization reserves budget immediately. That is the whole point: if decide() merely returned an opinion, two concurrent tool calls would both be told yes and the cap would mean nothing.

Usage

The core is framework-agnostic. You supply resolve, which turns a tool call into a PaymentIntent — or returns null for tools that do not spend.

import { createSpendHooks } from "@vaduno/agent";
import { AuditLedger, MemoryLedgerStore, MemorySpendLimiter, VadunoGuard } from "@vaduno/guard";

const guard = new VadunoGuard({
  policy: {
    id: "research-agent",
    version: 1,
    currency: "USD",
    limits: { perTransactionMinor: 2_00, perDayMinor: 20_00 },
    merchants: { allow: ["openai.com"] },
  },
  ledger: new AuditLedger(new MemoryLedgerStore()),
  limiter: new MemorySpendLimiter(),
});

const hooks = createSpendHooks({
  guard,
  resolve(call) {
    if (call.toolName !== "buy_api_credits") return null;   // not a spending tool
    const { orderId, cents } = call.input as { orderId: string; cents: number };
    return {
      id: orderId,                                          // the settlement key
      agentId: "research-agent",
      merchant: { id: "openai", url: "https://api.openai.com/v1/credits" },
      amount: { amountMinor: cents, currency: "USD" },
      category: "api-credits",
      rail: "stripe",
      requestedAt: new Date().toISOString(),
    };
  },
});

const decision = await hooks.decide({ toolName, input });
if (decision.kind === "deny") return refuse(decision.code, decision.reason);
// ... your framework runs the tool ...
await hooks.settled(decision.intentId, { ok: true });

resolve is yours, and it is treated as fallible

Only you know which of your tools move money and how much. Two rules:

  • null means "not a payment" — the guard allows it and records nothing. Do not use null to skip a check on a tool that does spend.
  • A throw is a DENY. If resolve fails you cannot tell what the tool would spend, and allowing an unknown spend is the one thing a spend firewall must never do. Override with onResolveError only if you are certain otherwise.

intent.id is the settlement key

Use a value stable for one logical payment and unique across payments. Reusing an id is treated as a replay: the tool is denied rather than run a second time, and the deny tells you which of three states the first attempt is in — ALREADY_EXECUTED (paid), ALREADY_ATTEMPTED (ran and failed), or INTENT_UNRESOLVED (outstanding, outcome unknown — reconcile before retrying).

Claude Agent SDK binding

import { bindClaudeAgentSdk, createSpendHooks } from "@vaduno/agent";

const sdk = bindClaudeAgentSdk(createSpendHooks({ guard, resolve }));

// PreToolUse  -> await sdk.preToolUse({ tool_name, tool_input })
// PostToolUse -> await sdk.postToolUse({ tool_name, tool_input, tool_response })

Status: this binding HAS now run against a live Claude Code session, and doing so found three mismatches that a green test suite could never have caught — because the tests and the code shared the same wrong assumption about the host:

  1. A non-payment tool returned permissionDecision: "allow", which short-circuits the host's own permission evaluation. Registered with a * matcher, this spend firewall auto-approved every other tool in the session. It now returns {} — no opinion.
  2. A failed tool never reaches postToolUse. It raises a separate failure event carrying error, so the failure heuristic could never fire and a failed payment was never settled — its authorization held budget until the rolling window aged out. Register postToolUseFailure too.
  3. Every event carries tool_use_id, the host's own correlation id, which is stabler than fingerprinting the tool input.

All three shipped in 0.5.0. Two tests in this package had faithfully asserted the first one back as correct.

The harness that found them is in examples/cli-agent-hook — a passive observer that records real payloads, and an enforcing hook that has demonstrably denied a real tool call in a live session. Re-run the observer when a host version changes; hook contracts drift.

Still true: no payment has run live. This proves the agent-side binding against a real host, not a payment against a real rail.

Other frameworks (Vercel AI SDK toolApproval, OpenAI Agents needsApproval, LangChain wrapToolCall) have the same decide-only shape. Use createSpendHooks directly and translate SpendDecision — that is all the SDK binding does.

Failure modes, and which direction they fail

Every ambiguous case here resolves toward over-holding budget, never overspending:

| Situation | What happens | Why | |---|---|---| | resolve throws | deny | An unknown spend is never allowed | | decide() throws | deny | A crashed check is not an approval | | Tool response unreadable | counted as spent | Guessing "failed" would free budget | | Tool ran and failed | counted as spent | The rail may have charged before failing | | Framework never calls settled | budget stays held | Starves its own cap; never leaks spend | | Duplicate intent.id | deny | Consume-once; the rail does not run twice | | Process restarts after a spend settled as executed | still counted, once the new process calls guard.hydrateFromLedger() on the same persistent ledger | The executed settle row carries the amount and currency, so hydration restores it into the caps. Qualifiers: a restart that never hydrates starts with empty state (pass requireHydration: true — see SECURITY.md); and rows hydration cannot restore — pre-0.3.0 settle() rows carried no amount, and a settle whose dedupe read failed lands without one (see the settle() docs) — are each reported in skippedUnparseableSpendRows instead of silently under-counting | | Process restarts after a spend settled as failed | the burned hold does NOT survive the restart | Hydration restores only executed rows. "Counted as spent" for a failed call is a live-process hold: after a restart the cap re-admits that amount even though the rail may have charged. Surviving this needs a persistent SpendLimiter, not the ledger |

The never-settled row deserves emphasis: an unsettled authorization keeps holding budget until its rolling window ages out. That is deliberate. Call settled for every allowed call, including failures.

What this does not do

  • It does not make an agent safe. It caps and records spending on the tools you route through it. A tool you forget to resolve is a tool with no limit.
  • It does not verify the payment happened. On this path the guard never sees the rail — it takes your word via settled, and the audit entry is marked selfReported so the ledger does not present a claim as an observation.
  • It does not stop a payment already in flight. Vaduno decides before, and records after. It never holds funds or the power to reverse them.

License

MIT