Install

Node.js and TypeScript

@pennipay/x402 adds Penni to Express, Hono, Next.js, Fastify and any fetch runtime with one middleware. No runtime dependencies.

Terminal
npm install @pennipay/x402

Set PENNI_SITE_ID and PENNI_API_KEY on your server, then add the middleware for your framework. Prices come from the dashboard; requests to free paths pass through untouched.

Add the middleware

import express from "express";
import { penni } from "@pennipay/x402/express";

const app = express();
app.use(penni({ siteId: process.env.PENNI_SITE_ID!, apiKey: process.env.PENNI_API_KEY! }));

// Priced in the dashboard: /premium/** at €0.05
app.get("/premium/report", (req, res) => res.json({ report: "...", payer: res.locals.payment?.payer }));

Next.js pages

To charge for pages as well as route handlers, use the middleware (middleware.ts; on Next 16 and later proxy.ts with export const proxy):

middleware.ts
import { penniMiddleware } from "@pennipay/x402/next";

export const middleware = penniMiddleware({ siteId: process.env.PENNI_SITE_ID!, apiKey: process.env.PENNI_API_KEY! });
export const config = { matcher: ["/premium/:path*"] };

Check your setup

Ask Penni what it will charge for a path before you send traffic:

check.ts
import { createPenni } from "@pennipay/x402";

const penni = createPenni({ siteId: process.env.PENNI_SITE_ID!, apiKey: process.env.PENNI_API_KEY! });
const cfg = await penni.config("/premium/report");
console.log(cfg.mode, cfg.ready, cfg.problems, cfg.match);
// test true [] { path: '/premium/report', pattern: '/premium/**', price: '0.01', free: false }

Settle before or after

By default the middleware settles first and then runs your handler, so content is only served once the money moved. With settle: "after" it runs your handler first and settles only after a 2xx response: a failing handler costs the payer nothing.

Options

OptionDefaultWhat it does
siteId, apiKeyrequiredFrom the dashboard.
settle"before""before" or "after", see above.
onUnavailable"closed"Penni unreachable or refusing the site: "closed" answers 503, "open" serves for free.
whenNotReadydashboard settingA priced path that can't take payments (paused site, no wallet, out of credits): "serve" or "block".
cacheTtlMs60000How long a path's price is cached.
resourceUrl(req)request URLThe URL in the 402. Set it behind a proxy that rewrites the host.
onPaid(event)noneCalled after each settlement with path, network, amount, payer and transaction.
onError(error)console.errorEvery problem, with a code: see errors.

The package also serves x402 v1 clients, Bazaar discovery metadata and Cardano's wait for confirmations without extra code.