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/enforceOne 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 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 naulonOn Django, add the middleware and tell it which responses are articles:
MIDDLEWARE = [
# after WhiteNoiseMiddleware, if you use it
"naulon.adapters.django.NaulonMiddleware",
]
NAULON = {"TOLLABLE": "blog.naulon_hooks.tollable"}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 == 200Then 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:
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:
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:
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.
- In WP Admin, open Plugins → Add New and search for naulon. It is in the WordPress Plugin Directory.
- Choose Install Now, then Activate.
- 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:
- Human? Browser-shaped requests return
NextResponse.next(), so your route renders normally, free. An unsure request is treated as human. - 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.
- A price comes back. The middleware short-circuits with
402 Payment Requiredand the payment terms. Your route never runs. - 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. A204means 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.fnreturns 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) orcitation; a citation prices higher.verifiedAgent(optional): the signature directory host your runtime verified for this request, such aschatgpt.com. It selects that company's own price from Prices for signed agents, and the quote names it back aspricedFor. 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=402The 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.
- Add
@naulon/enforceto your app and drop in the middleware above. What it tolls comes from your dashboard, so there is nothing site-specific to edit. - Put your
nln_live_key in the server environment asNAULON_API_KEY. - Keep the
matcheras the snippet has it. Paths outside your toll scope pass straight through, and the middleware also answers/license.xmland/.well-known/x402, which a narrower matcher would turn into 404s. - Watch a real agent request a gated article, get a
402, pay, and read, from its own IP, with no429. 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.