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

@openzk.app/purse

v0.1.1

Published

Anonymous prepaid API billing for AI apps: private ETH notes, per-request zero-knowledge burns, provable solvency.

Readme

@openzk.app/purse

React toolkit for the zkAPI protocol. A user funds a private ETH note, each metered AI call burns a slice of it behind a zero-knowledge proof, and nothing about the user ever reaches your server. No cards, no accounts, no email. This is the billing layer we run in production on openzk.app.

The crypto is the zkAPI protocol (Groth16 over BN254, Poseidon, Baby-JubJub). Note secrets and nullifier secrets are made in the browser and stay there. Your backend is never in the loop.

Install

npm install @openzk.app/purse react

Peer deps are react ^19 and @openanonymity/zkapi-browser-sdk ^0.2.0. The SDK peer is optional in the manifest because it is not on the public registry: it reaches you through zkAPI operator access, and you add it to your project yourself (a file: or link: entry, or an alias). Install exactly one copy of the SDK. It keeps a singleton internally and two copies will fight over it.

Browsers only. There is no Node or server-side mode; the proving worker needs a real browser.

Before it will boot

Three things, in order:

1. SDK assets under /zkapi/. The SDK ships a WASM proving worker plus proving keys and a browser config, and the client expects all of it same-origin at /zkapi/.... Generate at build time:

import { buildBrowserSdkAssets } from "@openanonymity/zkapi-browser-sdk/build";

await buildBrowserSdkAssets({
  outDir: "public/zkapi",
  publicPath: "/zkapi/",
  network: "mainnet",
});

The client boots from /zkapi/browser-config.json. If that 404s, nothing else runs, so check this first.

2. Pick a network. Read at build time from VITE_ZKAPI_NETWORK (mainnet, sepolia, localnet). If your stack has no Vite-style env, call configurePurse(deployment) before anything else instead.

3. Hand it a wallet. The purse never touches window.ethereum on its own. Whatever wallet layer you use, push the EIP-1193 provider in:

purseClient().setWalletProvider(provider);

The short version

import { useEffect, useState } from "react";
import { purseClient } from "@openzk.app/purse";
import { usePurse, useBurn, useExit } from "@openzk.app/purse/react";

function Billing() {
  const [address, setAddress] = useState("");
  const { snapshot, requestQuote, startDeposit, depositStatus } = usePurse();
  const { burn, streamingText } = useBurn();
  const { withdraw } = useExit();

  // wire your wallet provider however your app gets one
  useEffect(() => {
    if (walletProvider) purseClient().setWalletProvider(walletProvider);
  }, [walletProvider]);

  async function fund() {
    const quote = await requestQuote(5); // USD, pinned to a fresh Chainlink round
    if (quote) await startDeposit(quote, undefined, address);
  }

  return (
    <div>
      <div>balance {snapshot.balanceGwei} gwei ({snapshot.status})</div>
      <button onClick={fund} disabled={depositStatus !== "idle"}>fund $5</button>
      <button onClick={() => burn("one line on zkAPI", "be terse")}>ask</button>
      <pre>{streamingText}</pre>
      <button onClick={() => withdraw(address)}>withdraw</button>
    </div>
  );
}

Deposit goes to the vault onchain, the note commitment lands, and from then on the chain only ever sees nullifiers and proofs. Withdrawal is a mutual close back to any address.

Hooks

usePurse() funds and watches the note. Returns snapshot, journal, quote, requestQuote(usd: number), startDeposit(quote, provider?, userAddress), depositStatus, errorCode, errorMessage, reset.

useBurn() runs the spend loop: prove, lease an ephemeral inference key, stream from OpenRouter, settle the actual burn. Returns burn(prompt, systemPrompt?), streamingText, status, receipts, currentReceipt, selectedModel, setSelectedModel, models, plus the usual errorCode / errorMessage / reset. The receipt that lowers the note balance installs a beat after settlement, so the hook waits for the settled figure instead of eyeballing a balance diff. That number is the real onchain cost.

useExit() closes the note. Returns withdraw(recipientAddress), status, withdrawalDetails, errorCode, errorMessage, reset.

useModelCatalog() just returns the model list (readonly ModelOption[]). It starts from a curated set and merges the live OpenRouter catalog once it loads, bucketed into the protocol's spend tiers.

All four talk to one client instance. Subscribe from anywhere.

Client

const client = purseClient();
  • setWalletProvider(provider), before the first deposit
  • getSnapshot() / subscribe(fn): balance, status, latest root, note fingerprint
  • getHealth() / subscribeHealth(fn) / checkHealth(): operator root and mode
  • getJournal(): deposits, burns and withdrawals in one list
  • requestDepositQuote(usd): USD to ETH off a fresh oracle round
  • startDeposit(ethAmount, from, onStatus): full flow, wallet tx to note install
  • acquireAccess(sessionId, limitUsd, onProgress): prove and lease the inference key
  • settleLease(): settle the burn
  • withdrawMutual(destination, onStatus): close out
  • formatMoney(gwei): display string

configurePurse(deployment) swaps the target deployment. Useful for self-hosted operator stacks.

There are also small helpers exported for money (ethToGwei, formatEth, formatUsd, checkChargeCap), display (shortenFingerprint, formatNullifier, formatRelativeTime), model math (estimateSessionCostUsd, mapOpenRouterModel, mergeCatalog) and a retry wrapper. Import what you need from the root.

Errors

Every protocol error code has an entry in ERROR_CATALOG with a stable code, severity, plain user message and a recommended action. One recovery attempt fires automatically before anything surfaces to the UI. Show entry.userMessage, keep the raw protocol strings in your logs, not on screen.

import { getErrorCatalogEntry } from "@openzk.app/purse";
const entry = getErrorCatalogEntry("insufficient_balance");

Networks

mainnet (chain 1, default, real funds), sepolia (11155111, testnet), localnet (bring your own deployment and browser config). Charge caps and lease TTLs come from the deployment manifest, not from this package, and a mismatch fails closed.

One honest note: this is client tooling for a young protocol. Cap your per-session spend (acquireAccess takes the limit) and treat mainnet balances accordingly.

License

MIT