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

@zeroclickai/paywrap-adapter-hono

v0.0.13

Published

Hono adapter for @zeroclickai/paywrap: route-level MPP/x402 gates, pre-settlement checks, and Worker-friendly paid API context.

Downloads

1,658

Readme

@zeroclickai/paywrap-adapter-hono

Hono adapter for @zeroclickai/paywrap. Runs on Cloudflare Workers, Node, Bun, and anywhere else Hono runs.

Quickstart (Cloudflare Workers)

Two equivalent ways to wire up the paywrap app context. Use whichever matches your ctx lifetime.

Workers-style (ctx per request)

Worker env bindings (KV, secrets) are only available per request, so the ctx can't be built at module load. Pass a factory to createHonoApp:

import { createHonoApp, mppGated } from "@zeroclickai/paywrap-adapter-hono";
import { createPaywrapMpp, workersKvStore } from "@zeroclickai/paywrap/mpp";

type Env = { PAYWRAP_KV: KVNamespace; WALLET_PRIVATE_KEY: string; MPP_SECRET_KEY: string };

const app = createHonoApp<{ Bindings: Env }>((c) => {
  const mpp = createPaywrapMpp({
    walletPrivateKey: c.env.WALLET_PRIVATE_KEY as `0x${string}`,
    mppSecretKey: c.env.MPP_SECRET_KEY,
    publicBaseUrl: "https://my-worker.example.com",
    tempoRpcUrl: "https://rpc.tempo.xyz",
    store: workersKvStore(c.env.PAYWRAP_KV),
  });
  return { mppx: mpp.mppx, mppxChannelStore: mpp.channelStore };
});

app.post("/generate", mppGated({ scope: "gen:1", amount: 50_000n, intent: "charge" }), (c) => {
  return c.json({ result: "..." });
});

export default app;

Node-style (ctx known at module load)

import { createHonoApp, mppGated } from "@zeroclickai/paywrap-adapter-hono";
import { createPaywrapMpp } from "@zeroclickai/paywrap/mpp";

const mpp = createPaywrapMpp({
  walletPrivateKey: process.env.WALLET_PRIVATE_KEY as `0x${string}`,
  mppSecretKey: process.env.MPP_SECRET_KEY!,
  publicBaseUrl: process.env.PUBLIC_BASE_URL!,
  tempoRpcUrl: "https://rpc.tempo.xyz",
});

const app = createHonoApp({ mppx: mpp.mppx, mppxChannelStore: mpp.channelStore });

app.post("/generate", mppGated({ scope: "gen:1", amount: 50_000n, intent: "charge" }), (c) =>
  c.json({ result: "..." }),
);

Rolling your own middleware

If you can't use createHonoApp, you MUST set paywrapApp yourself in a pre-middleware. mppGated reads c.get("paywrapApp") — setting c.set("mpp", mpp) will NOT work:

app.use("*", async (c, next) => {
  const mpp = createPaywrapMpp({ ... });
  c.set("paywrapApp", {
    ctx: { mppx: mpp.mppx, mppxChannelStore: mpp.channelStore },
  });
  await next();
});

Persistent state on Workers

Workers isolates lose memory per restart. For state that must survive restarts:

  • Charge-intent: workersKvStore(env.PAYWRAP_KV) — free tier (100k reads + 1k writes per day + 1GB storage). No atomicity concerns for this pattern: challenge-id replay protection is the only shared state and KV's last-write-wins semantics don't break replay protection within the mppx window.
  • Session-intent (low concurrency): same as above, with the caveat in workers-kv.ts's docblock — two concurrent vouchers on the same channel can corrupt cumulativeAmount.
  • Session-intent (concurrent): Durable Object–backed store — future work, not yet shipped.

wrangler.toml:

[[kv_namespaces]]
binding = "PAYWRAP_KV"
id = "<your-namespace-id>"

[build]
nodejs_compat = true  # required for node:util (transitive via mppx)

x402 protocol

Same adapter, different protocol. x402Gated wraps @x402/hono's paymentMiddlewareFromHTTPServer for parity with mppGated — gate one route at a time without standing up a top-level x402 router. Settlement runs through a facilitator on Base (mainnet) or Base Sepolia (testnet); the seller wallet is just the receive address.

import { Hono } from "hono";
import { createPaywrapX402 } from "@zeroclickai/paywrap/x402";
import { x402Gated } from "@zeroclickai/paywrap-adapter-hono";

const x402 = createPaywrapX402({
  payTo: "0xYourSellerAddress",
  network: "base", // or "base-sepolia" for testnet
  // facilitator: { url: "https://x402.org/facilitator" } — default
});

const app = new Hono();
app.post("/generate", x402Gated(x402, { price: "0.005" }), (c) =>
  c.json({ result: "..." }),
);

You can mix protocols on the same Hono app — register mppGated on routes that should accept MPP credentials and x402Gated on routes that should accept x402 payments. Indexers reading /.well-known/paywrap.json already see per-route protocol: "mpp" | "x402".

For routes that need a custom accepts (multiple schemes/networks, non-USDC asset), pass acceptsOverride instead of price.

Non-Workers runtimes

The adapter is runtime-agnostic. On Node or Bun, use redisStore(new IORedis(...)) from @zeroclickai/paywrap/mpp for durable, linearizable state. workersKvStore is Workers-specific.

Typing custom data on c.var

mppGated already sets typed c.var.payer and c.var.verifiedCredential. If your preCheck needs to stash data for the handler — e.g. a parsed body or a derived id — use module augmentation so the keys are typed end-to-end:

declare module "@zeroclickai/paywrap-adapter-hono" {
  interface PaywrapBindings {
    sandboxInput?: { sandboxName: string; chargeHash: string };
  }
}

// preCheck:
c.set("sandboxInput", { sandboxName: "foo", chargeHash: "ff00" });

// handler — fully typed:
const input = c.get("sandboxInput");
if (input) console.log(input.chargeHash);

The as never cast pattern works for one-off prototypes (c.set("foo" as never, value as never)) but module augmentation is preferred for anything that ships.

Avoiding double-parsing in preCheck

Hono's c.req.json() memoizes per request — calling it again from the handler returns the cached parse. Same for c.req.formData() etc. So a preCheck that needs to read the body can safely call c.req.json() without forcing the handler to re-parse.

Validate before charging

For charge-intent routes, mppGated settles before your handler runs. Put cheap local validation in preCheck so callers are not charged for malformed JSON, unsupported options, name collisions, quota failures, or idempotent retries.

app.post(
  "/v1/render",
  mppGated({
    scope: "render:v1",
    intent: "charge",
    amount: 1000n,
    preCheck: async ({ c }) => {
      const body = await c.req.json().catch(() => null);
      if (!body || typeof body.diagram !== "string") {
        return { ok: false, status: 400, body: { error: "invalid_body" } };
      }
      c.set("renderBody" as never, body as never);
      return { ok: true };
    },
  }),
  async (c) => {
    const body = c.get("renderBody" as never) as { diagram: string };
    return c.json({ diagram: body.diagram, payer: c.var.payer });
  },
);

claimedPayer in preCheck is only a pre-verify hint. Use it to choose what to read, not to commit irreversible writes.

Per-request pricing

If price comes from Worker env or a route registry, build the gate in a tiny route middleware so the challenge amount matches runtime config and /.well-known/paywrap.json.

app.post(
  "/v1/render",
  async (c, next) => {
    const gate = mppGated({
      scope: "render:v1",
      intent: "charge",
      amount: BigInt(c.env.SKU_PRICE_USDC_MICRO),
      meta: { sku: "render:v1", pricingVersion: "1" },
    });
    return gate(c as never, next);
  },
  async (c) => c.json({ ok: true }),
);