Skip to content

In-app enforcement

When your origin’s edge rate-limits naulon: run the toll inside your own app, so the agent hits your origin directly. No shared IP, no 429.

On this page

By default the toll runs in front of your origin: an agent's request reaches us first, we price it, then proxy the read to you. All naulon traffic shares one egress IP, so if your origin sits behind an edge that rate-limits by IP (a serverless host's per-IP throttle, a WAF, a strict CDN), a busy site can start seeing 429s on legitimate reads.

In-app enforcement moves the decision into your app. A middleware runs on your origin; the agent's request arrives directly, from the agent's own IP, and the middleware either serves the page or answers 402. Payment and the catalogue still go through naulon over HTTPS, so you never hold funds and don't need a price list.

Use this mode when your origin's edge rate-limits naulon, or whenever your site runs its own code: a Node app, a Python app, or self-hosted WordPress. Your DNS doesn't change. If your site is static or hosted where you can't run code, use the front-of-origin toll instead; start there.

What you install

npm install @naulon/enforce

One middleware, wired to your framework's request pipeline. On Next.js App Router that's the root proxy file, proxy.ts on Next 16 (it was middleware.ts before; Next 16 renamed it and always runs it on the Node.js runtime, which is what the enforce kernel needs, so do not set a runtime in the config):

proxy.ts
// proxy.ts for Next 16 (older Next: middleware.ts). Runs on the Node.js runtime.
import { NextRequest, NextResponse } from "next/server";
import {
  naulonMiddleware,
  httpQuoteSource,
  httpPublisherConfigSource,
  httpObservationSink,
} from "@naulon/enforce";

// Toll stays OFF until NAULON_API_KEY is set. Fail open: never break the site or charge a human.
const apiKey = process.env.NAULON_API_KEY;
const toll = apiKey
  ? naulonMiddleware({
      // WHAT is tolled, and which crawlers you free, charge or refuse, read from your
      // dashboard, so a settings change takes effect without redeploying this file.
      config: httpPublisherConfigSource("https://gate.naulon.app/_naulon/enforce-config", apiKey),
      quote: httpQuoteSource("https://gate.naulon.app/_naulon/quote", apiKey),
      verifyUrl: "https://gate.naulon.app/_naulon/verify",
      apiKey,
      // Required. Without it an agent that already paid is charged again on its next read.
      licenseVerification: {},
      // Reports the verdicts only your process can see (free reads, refusals) to your
      // dashboard. Fire-and-forget; drop this line and the audit page stays empty.
      observe: httpObservationSink("https://gate.naulon.app/_naulon/observe", apiKey),
    })
  : null;

export async function proxy(request: NextRequest) {
  if (toll) {
    const { response, setHeaders } = await toll(request);
    if (response) return response; // 402 payment required / 403 blocked
    const res = NextResponse.next();
    for (const [k, v] of Object.entries(setHeaders ?? {})) res.headers.set(k, v);
    return res;
  }
  return NextResponse.next(); // toll disabled: pass through untouched
}

// Wake on everything but Next's own internals. What counts as an article is your dashboard
// setting, not a path list kept in step by hand. Anything out of scope passes straight through.
// Leave this as it is. The middleware also answers /license.xml (your licence) and
// /.well-known/x402 (the payment terms every 402 points agents at); a narrower matcher that
// drops either one makes it 404 on your domain.
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };

@naulon/enforce/next also ships createNaulonMiddleware(opts, NextResponse), a wrapper that handles NextResponse.next() and the headers when the proxy does nothing else. Use the core naulonMiddleware (above) when you combine the toll with other proxy logic, such as an auth or session refresh.

The key

NAULON_API_KEY is an nln_live_ key. It authenticates the hosted calls and can only quote and settle your own site's tolls.

Get one from Sites → Manage → Connection → Domains, on the domain's card, under Self-enforce in your app → Mint an API key. The secret is shown once; we store only a hash. If you lose it, mint another and revoke the old one. You can also build the same key in Settings → API & webhooks → Create key: pick the site, give it read access, and leave the expiry empty.

The four endpoints the middleware calls (/quote, /verify, /enforce-config, /observe) need only tenant.read. Don't use the 30-day Connect WordPress preset here; it is for the plugin.

Set it as a server-side variable. It must never reach the browser bundle: no NEXT_PUBLIC_ prefix, and don't read it in a client component.

@naulon/enforce is a separate package from @naulon/sdk (the credits-route and settlement helpers). It is framework-agnostic; @naulon/enforce/next is the Next.js adapter. It has no hard next dependency: you pass in your app's NextResponse, and outside Next the same core runs behind withNaulon, a generic fetch handler wrapper.

Python

The naulon package does the same job for Django, any WSGI app and any ASGI app. It has no runtime dependencies and needs Python 3.10 or newer.

pip install naulon

On Django, add the middleware and tell it which responses are articles:

settings.py
MIDDLEWARE = [
    # after WhiteNoiseMiddleware, if you use it
    "naulon.adapters.django.NaulonMiddleware",
]

NAULON = {"TOLLABLE": "blog.naulon_hooks.tollable"}
blog/naulon_hooks.py
def tollable(request, response):
    # Your post view sets request.naulon_post. Nothing else is ever charged.
    return getattr(request, "naulon_post", None) is not None and response.status_code == 200

Then set two server-side variables:

NAULON_GATE=https://gate.naulon.app
NAULON_KEY=nln_live_...

The key is the same one the Node middleware uses: read access to this site, no expiry (The key above).

On Flask or any other WSGI app, wrap the WSGI callable:

app.py
from naulon.adapters.wsgi import NaulonWSGI

app.wsgi_app = NaulonWSGI(app.wsgi_app)

On FastAPI, Starlette or any other ASGI app, add it as middleware:

main.py
from naulon.adapters.asgi import NaulonASGI

app.add_middleware(NaulonASGI)

Without a tollable hook, a WSGI or ASGI app tolls any 200 HTML response, and only an article in your catalogue is ever priced.

The middleware prices the URL your app sees, so behind a proxy that terminates TLS, make Django see the public host and scheme:

settings.py
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Set these only when your proxy overwrites both headers on every request. Otherwise the caller chooses them.

Two things to expect on the first run. The policy loads in the background, so the first second or so after a restart serves every request free; no request ever waits on naulon. And only articles in your catalogue are priced: a post naulon does not know about is served free, as it is in front of your origin.

WordPress

On self-hosted WordPress, a plugin does the same job in PHP: the agent's request reaches WordPress directly and the plugin decides there.

  1. In WP Admin, open Plugins → Add New and search for naulon. It is in the WordPress Plugin Directory.
  2. Choose Install Now, then Activate.
  3. Open naulon → Setup in the WordPress admin sidebar and choose Connect to naulon. You approve the site on naulon.app and come straight back, with the domain proven. Connect a site walks through it.

To paste a key instead, open Paste a key on the Setup screen and use a key from Settings → API & webhooks → Create key with the Connect WordPress preset.

Authors and co-authors

The plugin reads who wrote each post from WordPress, with nothing to set up. A post's own author is always credited. If the site runs Co-Authors Plus, PublishPress Authors or Molongui Authorship, every byline those plugins record is credited too, each an equal share of the toll, paid to the wallet on that author's profile.

A guest author with no WordPress account is credited without a wallet. Their share goes to your site wallet while Unclaimed author shares is on, which it is by default.

The plugin also lists your articles for naulon at /wp-json/naulon/v1/catalog, with every author under the same id they are paid under (wp-user-<number>, or wp-guest-<number> for a guest). That is what your site page's reports and author list are built from, so who an article credits and who it pays never disagree.

The other routes to your articles

WordPress hands out a post's text in more places than its page: the REST API (/wp-json/wp/v2/posts, and ?rest_route= on sites without pretty permalinks), RSS and Atom feeds, and listing pages such as your home page when the theme prints whole posts. For an agent the toll would charge, each of these now carries the teaser and a link to the article, which is where it pays. People, signed-in users and crawlers you let read free still get the full text everywhere.

If your site is behind naulon's routing, the gate does the same for the REST API and feeds even without the plugin, so those routes are covered either way.

Updates

The plugin updates like any other, from Dashboard → Updates or the notice on the Plugins screen, and Enable auto-updates works too. Your settings, your authors' wallet addresses and your earnings record live in the database and are kept. What changed in each version is on the listing's changelog.

If you installed from naulon.zip before the plugin was listed, you are already on the directory's update path: it is the same plugin under the same slug, so the next release shows up on your Plugins screen with nothing to reinstall.

Observe mode

Every new site starts by observing. The toll runs exactly as it would when charging: requests are classified, crawlers' identities are checked, and each agent read is priced. The read is then served instead of answered with a 402, and reported with the price it would have paid. No payment is taken, and a payment an agent presents anyway is not settled.

Blocked crawlers and uses your terms prohibit are still refused. Observe stops charging, never a refusal you set.

You don't need a payout wallet to observe. Your site page shows, for the last 30 days, how many agent reads were priced, what they would cost at your price, and how many agents said they would pay it: a crawler that sends crawler-max-price covering your price, or one that presents a payment. The middle figure is an estimate, not money you are owed. When the numbers are worth it, add a payout wallet and choose Start charging at the top of the site page.

Reads are half of it. The same page also shows how often agents were quoted your articles by naulon's search, its agent tools and its pay-per-answer door, and how many of those quotes judged an article a direct answer to the question asked. While you observe, agents are offered your articles as free reads with the price they will cost once you charge, and the page adds what those quotes were worth at your prices. These counts never include who asked or what they asked. Every plan sees the totals; the breakdown by article, with how often each was a direct answer and how often it sold, is on Studio and up.

Below that, What agents ask that nothing answers lists topics agents asked about that no licensed source on naulon answered, where your catalogue already covers at least half of the topic's words. A topic appears only once five different callers asked it, and never with a question beside it. It is on Studio and up; Enterprise also sees the same list across every site on naulon.

Charging works the other way round: an article is offered with a price only when a read can be paid, so a site that charges without a payout wallet is left out of those answers until it has one.

On WordPress, observe needs naulon plugin 0.6.4 or later. An older plugin keeps charging.

If you run the middleware yourself, the switch is tollMode: "observe" on the publisher config. Leaving it unset means charge.

How a request flows

Same decision as the front-of-origin toll, made in your runtime:

  1. Human? Browser-shaped requests return NextResponse.next(), so your route renders normally, free. An unsure request is treated as human.
  2. Agent, on a tolling path? The middleware asks for a quote. No quote (the slug isn't gated, or has no wallet while charging) means a free read, and your route renders. While observing, the read is priced and served, and nothing below runs.
  3. A price comes back. The middleware short-circuits with 402 Payment Required and the payment terms. Your route never runs.
  4. The agent pays. It retries with a signed payment. The middleware POSTs that to the verify endpoint; we settle it on-chain, buyer → author, and return a receipt plus a short-lived license. The middleware attaches the receipt headers and lets your route render. The agent can re-read that piece under the license without paying again.

The 402 is byte-identical to the one the front-of-origin toll produces, because the price comes from the same resolver.

Where prices come from

On WordPress the plugin asks the hosted quote endpoint, and your credits endpoint answers who wrote each post and where the money goes.

In the Node middleware, the quote option supplies price and payees from one of two sources:

  • Hosted: httpQuoteSource(url, key). Each gated read asks the quote endpoint what the resource costs. Use this when naulon already knows your catalogue (for example, we crawled it), so you keep no price list in your app. A 204 means a free read.
  • Your own data: localQuoteSource(fn). If your app already knows its authors and prices, from a credits API or your database, wrap that lookup. fn returns the price and payee wallets for a slug, or nothing for a free read. There is no round-trip per quote; only the payment leaves your app.

Either way a quote carries payTo addresses and a price, never a wallet key. The split between co-authors is computed the same as everywhere else: integer micro-USDC, each share settling to its own wallet (settlement says when).

What still runs through naulon

Three HTTPS calls, all authenticated with your nln_live_ key and scoped to your tenant by the resource host. The first two are the toll; the third puts your traffic on your Audit page:

GET /_naulon/quote

Price a resource. Called by httpQuoteSource.

GET https://gate.naulon.app/_naulon/quote?resource=<url>&slug=<slug>&kind=read|citation
Authorization: Bearer nln_live_...
  • resource: the full URL being decided. Its host must be your tenant.
  • slug: the article identifier.
  • kind: read (default) or citation; a citation prices higher.
  • verifiedAgent (optional): the signature directory host your runtime verified for this request, such as chatgpt.com. It selects that company's own price from Prices for signed agents, and the quote names it back as pricedFor. Send it only for a signature you verified, never for a user-agent string.

Responses: 200 with the quote, 204 for no toll (free read), 401 for a bad key, 403 if the resource isn't yours.

The quote's price is the author leg. extraLegs carries every other transfer the same read requires, and naulon charges an operator fee, so the amount a buyer authorizes is price plus every entry in extraLegs. A 402 that offers only price still works, but the legs you leave out are never collected.

If you are not on Node, add &build=402 and you get the whole 402 built for you:

GET https://gate.naulon.app/_naulon/quote?resource=<url>&slug=<slug>&build=402

The response gains x402: { header, legs }. Use it instead of porting the leg and rounding math into another language, where a mismatch would go unnoticed.

POST /_naulon/verify

Settle a presented payment. Called by the middleware after an agent pays.

POST https://gate.naulon.app/_naulon/verify
Authorization: Bearer nln_live_...
Content-Type: application/json

{ "payment": "<signed payment>", "legs": [...], "quote": {...}, "resource": "<url>" }

legs and quote are what the middleware built from the price; we verify the payment against them and settle buyer → author directly. Responses: 200 with the receipt (settlementRef, payer, and the response header to echo back), 400 for a malformed body, 401 for a bad key, 403 if the resource isn't yours or the site is suspended.

POST /_naulon/observe

Report what your runtime decided, so it shows up on your Audit page. The toll works without it, but then the page shows only paid reads, because nothing else passes through us.

POST https://gate.naulon.app/_naulon/observe
Authorization: Bearer nln_live_...
Content-Type: application/json

{
  "resource": "<url>",
  "slug": "<slug>",
  "verdict": "denied",
  "classifiedAs": "agent",
  "kind": "read",
  "priceMicro": 1000,
  "at": 1754120000000,
  "agent": { "ua": "GPTBot/1.0", "classifyReason": "ua-pattern:bot" }
}

Send one object or an array of up to 50. verdict is one of served-free, agent-reread, denied, blocked: the four outcomes only your runtime saw. priceMicro is integer micro-USDC and drives the "earnings missed" figure. Everything but resource, verdict and classifiedAs is optional; at defaults to now and is clamped if it's wildly off.

On a person's read (classifiedAs: "human"), add referrerHost with the host of the other site that sent them, for example chatgpt.com, and your Audit page counts the readers each AI answer engine sends beside the requests its agents make. Send the host only, never the full referring URL, and leave it out when the referrer is your own site. @naulon/enforce does this for you.

paid and payment-failed are refused here. We record those from the settle outcome at /verify, so your dashboard only reports money that moved. Paid reads appear on your Audit page whether or not you wire this up.

Responses: 202 with { ok: true, accepted: n }, 400 for a malformed report or a money verdict, 401 for a bad key, 403 if the resource isn't yours.

To turn it on in the Node middleware:

import { naulonMiddleware, httpObservationSink } from "@naulon/enforce";

naulonMiddleware({
  // ...
  observe: httpObservationSink("https://gate.naulon.app/_naulon/observe", apiKey),
});

One request per decision, fire-and-forget. It can't delay or fail a reader's page; if we're unreachable you only lose visibility.

The middleware makes these calls for you. What leaves your app is a signed payment and a resource URL, under a key scoped to your site. /observe adds the User-Agent and the verdict. No reader identity, IP or page content is sent.

Going live (Node apps)

On WordPress, follow the WordPress section instead.

  1. Add @naulon/enforce to your app and drop in the middleware above. What it tolls comes from your dashboard, so there is nothing site-specific to edit.
  2. Put your nln_live_ key in the server environment as NAULON_API_KEY.
  3. Keep the matcher as the snippet has it. Paths outside your toll scope pass straight through, and the middleware also answers /license.xml and /.well-known/x402, which a narrower matcher would turn into 404s.
  4. Watch a real agent request a gated article, get a 402, pay, and read, from its own IP, with no 429. See settlement for what lands and where.

The first agent request also switches your dashboard from "waiting" to connected, because every gated request calls the quote endpoint. A human page view doesn't, since humans read free without a quote. If the dashboard still says "waiting", send an agent-shaped request yourself: curl -A GPTBot https://your-site/articles/<a-gated-slug> should return 402.