@naulon/enforce
v0.2.0
Published
Runtime-agnostic naulon toll-decision kernel + in-app enforcement middleware. The neutral low-level core shared by @naulon/tollgate (the gate) and @naulon/sdk (the publisher SDK) — no dependency cycle.
Readme
@naulon/enforce
The runtime-agnostic toll-decision kernel plus the in-app enforcement middleware.
This is the neutral low-level core that both @naulon/tollgate (the gate shell —
the reverse-proxy that boots createApp) and @naulon/sdk (the publisher SDK)
sit above, with no dependency cycle. It depends only on @naulon/shared (and
viem, for the holder-of-key proof). The heavy settlement path — the Circle
facilitator, the pending-leg drain — stays in @naulon/tollgate; nothing here
imports @circle-fin/x402-batching.
Why it exists
decide() is one pure function: given a web Request and a known publisher, it
returns a verdict — serve free, refuse, or 402 with the payment legs — and
performs no side effects (no proxy, no settle, no observe). Extracting it lets
two very different runtimes reach the same verdict:
- The gate (
@naulon/tollgate) runsdecide()inside its Hono reverse proxy. - In-app middleware runs the identical
decide()in the publisher's own app, so an agent's request reaches the origin directly instead of routing through the fleet's single egress IP (which an origin edge can rate-limit).
Both build a byte-identical 402, because they share this code.
Exports
@naulon/enforce
The decision kernel and the framework-agnostic middleware core:
decide(input)— the pure verdict function.naulonMiddleware(opts)— takes aRequest, returns{ response, setHeaders }: aResponseto short-circuit (402/403), ornullto let the app render (withsetHeadersto attach to the app's response on a paid pass).withNaulon(handler, opts)— wrap a genericfetchhandler.localQuoteSource(fn)/httpQuoteSource(url, key)— pluggable price + payees.- The classification, Web Bot Auth, nonce, and proof primitives (
classify,verifyBotAuth, …) and the x402 build side (build402,buildRequirements). - Cloudflare pay-per-crawl interop —
formatCrawlerPrice,parseCrawlerPrice,declaredCrawlerBudget,crawlerBudgetVerdict,totalChargedMicro, and the four header constants (crawler-max-price/crawler-exact-priceon the request,crawler-priceon a402,crawler-chargedon a paid200). A crawler already fluent in that vocabulary can price your origin with no change on its side. You advertise in their vocabulary and settle in ours — x402/USDC, buyer→author — so nothing here moves money or changes who is charged. Prices render at full precision rather than Cloudflare'sUSD XX.XX: a citation toll is often sub-cent, and(0.001).toFixed(2)would advertise a free read. It lives here rather than intollgateso a self-hosting publisher on the SDK gets it too.
@naulon/enforce/next
createNaulonMiddleware(opts, NextResponse)— the Next.js App Router adapter. It has no hardnextdependency; you injectNextResponse(your app already has it), keeping the core framework-agnostic.
Usage
// middleware.ts (Next.js App Router)
import { NextResponse } from "next/server";
import { createNaulonMiddleware } from "@naulon/enforce/next";
import { httpQuoteSource } from "@naulon/enforce";
export const middleware = createNaulonMiddleware(
{
publisher: { id: "your-site", articlePrefixes: ["articles"] },
quote: httpQuoteSource("https://<your-control-plane>/_naulon/quote", process.env.NAULON_API_KEY!),
verifyUrl: "https://<your-control-plane>/_naulon/verify",
apiKey: process.env.NAULON_API_KEY!,
},
NextResponse,
);
export const config = { matcher: ["/articles/:path*"] };quote and verifyUrl point at whatever runs the money + catalog legs — the
managed control plane, or your own self-hosted POST /_naulon/verify +
GET /_naulon/quote. The middleware never holds funds: it forwards the buyer's
signed payment to verifyUrl, which settles buyer → author directly.
Layering
Arrows point to what a package depends on:
flowchart TD
tollgate["@naulon/tollgate<br/><i>gate shell — runs decide() in its reverse proxy</i>"] --> enforce
enforce["@naulon/enforce<br/><i>this package — decision kernel + middleware</i>"] --> shared["@naulon/shared"]A publisher vendors @naulon/enforce directly (it builds to its own dist/
tarball) and wires the middleware; the gate consumes the very same package, which
is what guarantees both reach an identical verdict. @naulon/enforce is
deliberately NOT re-exported through @naulon/sdk — @naulon/shared imports the SDK
and re-exports it, so an sdk → enforce edge would close the loop
sdk → enforce → shared → sdk, a declaration-build cycle. Keeping enforce standalone
(a second, small dependency alongside the SDK) avoids that and keeps the package
graph a clean chain.
