Node.js and TypeScript
@pennipay/x402 adds Penni to Express, Hono, Next.js, Fastify and any fetch runtime with one middleware. No runtime dependencies.
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 }));
import { Hono } from "hono"; import { penni, type PenniVariables } from "@pennipay/x402/hono"; const app = new Hono<{ Variables: PenniVariables }>(); app.use("/api/*", penni({ siteId: process.env.PENNI_SITE_ID!, apiKey: process.env.PENNI_API_KEY! })); app.get("/api/forecast", (c) => c.json({ tomorrow: "sunny", payer: c.get("payment")?.payer }));
// app/api/report/route.ts import { getPayment, withPenni } from "@pennipay/x402/next"; const options = { siteId: process.env.PENNI_SITE_ID!, apiKey: process.env.PENNI_API_KEY! }; export const GET = withPenni(async (req: Request) => Response.json({ payer: getPayment(req)?.payer }), options);
import Fastify from "fastify"; import { penni } from "@pennipay/x402/fastify"; const app = Fastify(); app.addHook("onRequest", penni({ siteId: process.env.PENNI_SITE_ID!, apiKey: process.env.PENNI_API_KEY! })); app.get("/premium/report", async (req) => ({ report: "...", payer: req.payment?.payer }));
import { getPayment, withPenni } from "@pennipay/x402/fetch"; const handler = (req: Request) => Response.json({ payer: getPayment(req)?.payer }); // Cloudflare Workers export default { fetch: withPenni(handler, (req, env: Env) => ({ siteId: env.PENNI_SITE_ID, apiKey: env.PENNI_API_KEY })), }; // Bun: Bun.serve({ fetch: withPenni(handler, options) }) // Deno: Deno.serve(withPenni(handler, options))
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):
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:
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
| Option | Default | What it does |
|---|---|---|
siteId, apiKey | required | From the dashboard. |
settle | "before" | "before" or "after", see above. |
onUnavailable | "closed" | Penni unreachable or refusing the site: "closed" answers 503, "open" serves for free. |
whenNotReady | dashboard setting | A priced path that can't take payments (paused site, no wallet, out of credits): "serve" or "block". |
cacheTtlMs | 60000 | How long a path's price is cached. |
resourceUrl(req) | request URL | The URL in the 402. Set it behind a proxy that rewrites the host. |
onPaid(event) | none | Called after each settlement with path, network, amount, payer and transaction. |
onError(error) | console.error | Every 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.