Humans read free. Machines pay.
A pay-per-read toll for the agentic web. Drop it in front of any site; AI agents and crawlers pay a USDC nanopayment to read or cite an article, and it settles to the author's own Circle Gateway balance, which only their wallet can withdraw.
People should read for free. But large language models now consume writing at
scale and pay nothing for it: the cost lands on the author, the benefit on the
crawler. naulon flips that one exchange: a human request passes straight
through, untouched; a machine request gets an HTTP 402 Payment Required and has
to settle a tiny USDC payment before it reads. The fare is the naulon, the
old coin you paid to cross.
The toll is what keeps the work open: machines subsidize the free human read.
Built on the Circle nanopayment rail, Arc-first on Arc Network, with the wider set of Circle Gateway chains in the network registry, so a single read can cost a fraction of a cent and still settle.
flowchart TD
Req([Incoming request]) --> Cls{"agentDetect:<br/>human or machine?"}
Cls -->|human| Free[["Proxied through, untouched: free read"]]
Cls -->|machine| Bill["402 Payment Required<br/>price · author wallet(s) · signed nonce"]
Bill --> Sign["Agent signs a USDC payment,<br/>echoes the nonce, retries with payment-signature"]
Sign --> Verify{"Verify via Circle Gateway"}
Verify -->|valid| Serve[["Serve content + mint a Citation License,<br/>record who earned what"]]
Verify -->|replayed / invalid| Bill
linkStyle 0 stroke-width:2px
linkStyle 1 stroke:#3fb950,stroke-width:2px
linkStyle 2 stroke:#d29922,stroke-width:2px
The piece that makes it interesting: attribution is the payout rule. An article's credits graph, who wrote it and in what proportion, is how the money splits. Co-authored pieces split automatically, and a credit can itself be a collective that splits again among its members. No invoices, no manual payouts.
The split is recursive: a payee can itself be a collective that divides its cut
again, all the way down. This is the real credits graph from
examples/cascade (how-vaccines-teach),
resolved by the split algorithm in @naulon/shared:
flowchart TD
Toll["Toll paid: $0.006"]
Toll -->|weight 3| Desk["the-immunology-desk<br/><i>collective</i> · $0.0045"]
Toll -->|weight 1| Mira["mira · $0.0015"]
Desk -->|weight 1| Oko["okonkwo · $0.0015"]
Desk -->|weight 2| Bench["the-bench-team<br/><i>collective</i> · $0.0030"]
Bench -->|equal| Pet["petrova · $0.0015"]
Bench -->|equal| Had["haddad · $0.0015"]
Every leaf is a direct author payout, tracked per (event, author), and recorded
whether or not it clears the payout floor yet. The intermediate collectives never
hold the money; they're only a rule for how the cut divides.
Requires Node ≥ 22.
make install
make demo # the whole loop offline: origin → toll → agent pays → settleTo put a gate in front of your own site, naulon init asks a handful of
questions and writes a coherent .env plus a starter credits.json, with no
hand-editing of the full example:
npx naulon init # → .env + credits.json, then: make devIt runs in mock-settlement mode by default (no wallet or API keys), refuses to
overwrite files you've edited unless you pass --force, and takes --yes with
flags for a non-interactive/CI run.
Or set it up by hand:
cp .env.example .env # runs in mock-settlement mode with no creds
make dev # stub origin :3000 + tollgate :8402 + dashboard :8403
make tollgate # just the reverse proxymake help lists every target.
The gate and console ship as one image, so a host with Docker needs neither this repo nor Node:
curl -O https://raw.githubusercontent.com/naulonapp/naulon/main/docker-compose.yml
npx -p @naulon/sdk naulon init # writes .env + credits.json (or write them by hand)
mkdir -p config && mv credits.json config/
docker compose up -d # gate :8402 · console :8403 (loopback only)ghcr.io/naulonapp/naulon is public, with no registry login. Both services run the same
image under an unprivileged user and share one ledger volume; your credits.json is
mounted read-only from ./config rather than baked in, because a gate that fell back
to an example's wallets would settle real money to a fixture address. Compose follows
:latest, which each release moves; pin one with NAULON_TAG=v0.7.0 in .env, or
track main between releases with NAULON_TAG=edge. v0.7.0 is the first tag carrying an
image, because the workflow that builds one postdates every tag before it, so v0.6.0 and
earlier name releases that exist on npm and in git, never in the registry.
The console binds wide inside its container (127.0.0.1 there is unreachable from your
host) and is published on your loopback only. Because the socket is no longer evidence
of privacy, it requires a credential before it serves: set CONSOLE_ADMIN_PASSWORD
in .env to seed a first administrator that must change its password on first sign-in,
or DASHBOARD_AUTH=user:pass for HTTP Basic.
Contributors who want the image built from a working tree instead:
make docker-build.
Two commands, both offline. doctor reads your .env, validates the credits source, and
probes the gate: a browser must read free, an agent must get a 402.
npx -p @naulon/sdk naulon doctorselftest goes the rest of the way: it pays the 402, reads the article, checks the citation
licence that came back, and proves the same payment cannot be replayed:
npx -p @naulon/sdk naulon selftest ✓ manifest self-describing toll at /essays/ · read 1000 atomic
✓ human browser UA → HTTP 200, straight through
✓ quote 402 · 1000 atomic → 0x1111…1111
✓ pay HTTP 200 · settled with a payment-response
✓ licence citation licence for "hello-world" naming 1 payee(s)
✓ replay the spent payment bought nothing a second time
✓ citation 5000 atomic (5× a read) · paid, HTTP 200
It drives the path prefix your gate advertises, not a hardcoded one, and picks the first
article in your credits (--slug to choose). The payment is the offline mock signature, so
nothing moves, and against a gate already in gateway mode it says so rather than failing.
Watch the toll work (mock settlement, so no wallet or API keys needed):
# a human reads for free → proxied straight through
curl -H 'accept: text/html' -A 'Mozilla/5.0' localhost:8402/essays/on-stillness
# an agent gets a bill
curl -A 'python-requests' localhost:8402/essays/on-stillness
# → 402 { x402Version: 2, resource, accepts: [{ scheme, network, asset, amount, payTo, extra: { nonce } }] }
# a co-authored piece, cited rather than read: 5× the price by default
# (CITATION_MULTIPLIER), split 2:1
curl -A 'python-requests' -H 'x-naulon-kind: citation' localhost:8402/essays/the-naulonThe agent then signs a payment, echoes the nonce back in a payment-signature header,
and retries. The gate verifies it, serves the article, and records who earned
what.
The Wayfarer automates that whole loop, and makes a real budget decision at each step rather than paying for everything:
npm run tollgate & # in one shell
npm run wayfarer -- "payment and passage" # in anotherappraisal:
· the-naulon: relevance 1.00, shares 2/2 topic terms (payment, passage)
· on-stillness: relevance 0.00, no topic-term overlap
decisions:
[PAY] the-naulon, relevance 1.00 @ $0.005 (density 200); $0.095 left
[SKIP] on-stillness, relevance 0.00 below floor 0.35
✓ paid $0.005000 for the-naulon
→ earnings: ava 0.00333 / guest 0.00167 (the 2:1 co-author split, settled)
It ranks candidates by relevance-per-dollar, buys greedily under the budget down
to a relevance floor, reuses anything already cached, and logs why for each:
the reasoning is the artifact. It runs offline against mock settlement; set
OPENAI_API_KEY for LLM appraisal and answer synthesis instead of the keyword
heuristic.
npm run dashboard # http://localhost:8403Your window onto the gate: health, live toll traffic (served free / denied /
paid), settlement earnings, and a config-sanity panel, so you can see your proxy
actually working. Two of its pages also write: Content edits credits.json (who
gets paid) and Crawlers edits the crawler policy (who reads free, pays, or is
refused). It reads the gate's observation log (set
OBSERVATIONS_BACKEND=jsonl) and event ledger. Private on 127.0.0.1 by default;
making it reachable at all, through a wider bind, a reverse proxy or a serverless deploy,
needs DASHBOARD_AUTH, and DASHBOARD_PUBLIC=true serves only a masked public
earnings page. Full guide: docs/operating.md.
npm run attribution # one settlement pass over the ledgerSub-cent tolls aren't worth settling one at a time, so the service accrues each
author's share across many events and only cuts a payout once it clears
MIN_PAYOUT_USDC, carrying the rest forward. Settlement is tracked per
(event, author), so a co-author whose small share is still below the floor
keeps accruing while their co-author gets paid; nothing is double-paid or lost.
Mock settlement runs offline; real Circle Gateway batching needs a funded testnet
wallet to exercise live (PAYMENT_MODE=gateway).
Nothing here is tied to a particular publisher. You wire up two things:
ORIGIN_URL: the site to sit in front of.- A credits source: how an article slug maps to its author wallet(s).
Point
CREDITS_API_URLat your CMS (it serves/credits/:slug), or ship a staticcredits.json. For anything custom, implement the one-methodCreditsResolverinterface from@naulon/shared.
A complete worked example lives in
examples/meridian/, a fictional essays publisher.
Copy that folder, swap the origin and credits, and you have your own toll.
examples/cascade/ is a second adapter for a different
kind of publisher (a different origin, path prefix, and a deeper credits graph),
proving the same core is publisher-agnostic with zero code changed.
For building the publisher side, the kit @naulon/sdk packages both
endpoints, the credits resolver and the HMAC-verified webhook receiver, with
drop-in adapters for Next.js (/next) and Express (/express), and a
naulon-kit check CLI that conformance-tests your live /credits endpoint against
the contract. Start with the
integration guide; the two wire contracts are
credits-api.md and
settlement-notifications.md, and a runnable consumer is
in packages/sdk/examples/next-credits/.
Point the gate at one or more of your own HTTPS endpoints and it POSTs a signed
settlement.completed after every paid read. Set NAULON_WEBHOOK_ENDPOINTS to a
JSON array:
NAULON_WEBHOOK_ENDPOINTS='[{"url":"https://you.example/naulon-hook","secret":"whsec_…","events":["settlement.completed"],"hostFilter":null}]'
Each endpoint gets a Naulon-Signature: t=<unix>,v1=<hex> header: HMAC-SHA256 over
${t}.${rawBody}, keyed by your secret. Verify it with verifyPayload from
@naulon/sdk, or take the whole receiver (verify + the mandatory dedupe) with
createWebhookReceiver. Endpoints must be HTTPS; delivery retries with backoff and is
a no-op when the env is unset. Full contract:
settlement-notifications.md.
This is the only settlement-notification path. The gate used to also POST a
signed settlement straight at the publisher's origin; that wire was deleted (WH-1 P3)
along with CREDITS_SETTLEMENT_SECRET and the eleven SETTLEMENT_* knobs that tuned
it. One fact, one delivery mechanism.
A small npm-workspaces monorepo. Each piece is independent and runs on its own.
| Package | What it does |
|---|---|
tollgate |
The x402 reverse proxy: human/agent detection, the 402 challenge, payment verification, and the attributed-event log. |
wayfarer |
An autonomous research agent that decides which articles are worth paying to cite under a budget, then pays. |
attribution |
Batches sub-cent tolls per wallet and settles author payouts (mock, or real Circle Gateway via PAYMENT_MODE). |
dashboard |
The operator console: gate health, live toll traffic, earnings, and config sanity (plus an opt-in public earnings page). |
Under those sit three libraries the runnable pieces share:
sdk (the publisher contract + crawl engine),
shared (domain types, config, the split algorithm), and
enforce (the runtime-agnostic toll-decision kernel). There's
also wayfarer-mcp, which exposes the paying agent as an
MCP server.
How the packages layer. Arrows point to what a package depends on. The dependency graph is strictly one-directional; nothing lower reaches up.
flowchart TD
tollgate["tollgate<br/><i>the gate</i>"]
wayfarer["wayfarer<br/><i>paying agent</i>"]
wmcp["wayfarer-mcp<br/><i>MCP server</i>"]
attribution["attribution<br/><i>settlement</i>"]
dashboard["dashboard<br/><i>console</i>"]
enforce["enforce<br/><i>decision kernel</i>"]
shared["shared<br/><i>contract + split math</i>"]
sdk["sdk<br/><i>publisher kit + crawl</i>"]
tollgate --> enforce
tollgate --> sdk
tollgate --> shared
wmcp --> wayfarer
wayfarer --> shared
attribution --> shared
dashboard --> sdk
dashboard --> shared
enforce --> shared
enforce --> sdk
shared --> sdk
packages/
sdk/ publisher contract · framework adapters · crawl engine
shared/ types, config, attribution + split math
enforce/ runtime-agnostic toll-decision kernel + in-app middleware
tollgate/ proxy · agentDetect · x402 · pricing · credits · eventLog
wayfarer/ the paying agent
wayfarer-mcp/ the paying agent as an MCP server
attribution/ settlement + payouts
dashboard/ operator console
examples/
meridian/ worked example: a fictional publisher adapter
cascade/ second adapter: proves the core is publisher-agnostic
scripts/ dev stack runner · self-contained demo · wallet generator
Makefile · Dockerfile · docker-compose.yml
npm run lint # typecheck the whole workspace
npm test # unit tests (attribution splits, dust-free settlement, …)
npm run tollgate # or: wayfarer · attribution · dashboardEverything runs straight from TypeScript via tsx,
with no build step while developing.
Everything above runs in mock settlement so you can develop offline. The real
rail is wired to the Circle Gateway batching SDK (@circle-fin/x402-batching),
Arc-first. Flip PAYMENT_MODE to switch:
make generate-wallets # a buyer + author wallet; fund the buyer via the Circle faucet on Arc
# put PAYMENT_MODE=gateway and BUYER_PRIVATE_KEY in .env
PAYMENT_MODE=gateway make wayfarer TOPIC="payment and passage"- Seller (tollgate):
BatchFacilitatorClient.verify/.settleagainst the Arc GatewayWallet (0x0077777d7EBA…, networkeip155:5042002, Arc testnet). No seller key, because Gateway settles the buyer's deposit into the author's own Gateway balance. Custody-free: only the author's wallet can withdraw it, at naulon.app/earnings or with Circle's Gateway API directly. If you build an author screen, link the former there. - Buyer (wayfarer):
GatewayClient.depositonce, then.pay()per citation runs the full deposit-backed 402 flow (gasless, batched, <500ms finality). - One payment, one
payTo. x402 settles to a single address, so the on-chain leg pays the article's primary author; the recursive co-author split is the attribution layer's job (its onward payouts). The split is always recorded. - Network.
SETTLEMENT_NETWORKpicks the chain:arcTestnet(default),baseSepolia, orbase. Arc is the first-class rail (it ships the Memo contract that carries the citation reference); the full Circle chain registry, Arc mainnet included, lives inshared/src/networks.ts, and chains light up as live settlement is verified on each.
sequenceDiagram
autonumber
participant A as Agent (wayfarer)
participant G as Gate (tollgate)
participant C as Circle Gateway (Arc)
participant Au as Author wallet
A->>G: GET /essays/the-naulon
G-->>A: 402 · price · payTo · nonce · Link: /.well-known/x402
Note over A: appraise relevance-per-dollar, decide to pay under budget
A->>C: GatewayClient.pay(): deposit-backed, gasless
A->>G: retry + payment-signature (signature, nonce)
G->>C: BatchFacilitatorClient.verify + settle
C->>Au: USDC settles buyer → author (custody-free)
G-->>A: 200 · content + Citation License Token
The header contract (PAYMENT-REQUIRED / payment-signature / PAYMENT-RESPONSE)
and Arc constants mirror circlefin/arc-nanopayments, so a stock Gateway client
pays this gate unmodified. Both modes typecheck against the real SDK; the gateway
path needs a funded testnet wallet to exercise live.
Self-describing toll. The gate advertises its own terms so an agent can find
them without being told the endpoint out of band. Every 402 carries a Link: </.well-known/x402>; rel="payment" header, and GET /.well-known/x402 returns a
machine-readable manifest: article path prefixes, read and citation price, the Arc
network + USDC asset, and the JWKS/verify URLs. It names no author wallet: payTo
is resolved per article from the credits graph at payment time.
curl localhost:8402/.well-known/x402
# → { x402Version, humansReadFree, resources:{pathPrefixes,kinds}, payment:{network,asset,price}, license }make demo and make dev need nothing but a JSONL ledger and in-process state. To
host the gate + dashboard on a serverless platform (Vercel) in front of a real
site, swap those two pieces of state for Supabase (EVENTS_BACKEND=supabase,
NONCE_BACKEND=supabase) so every instance shares one ledger and nonce set.
Step-by-step (Vercel projects, Supabase schema, DNS): DEPLOY.md.
Self-host or hosted. Everything in this repo runs standalone, so you own the gate. If you'd rather not run the infrastructure, naulon.app operates a managed fleet that speaks the same protocol: you point your site at it and declare your credits URL exactly as the integration guide shows, then serve the same two endpoints. Same toll, same custody-free settlement, and nothing here is held back to push you toward it.
- Custody-free. Payments go agent → author. The toll never pools USDC in a wallet the operator controls, which keeps them clear of money-transmission rules.
- Conservative classifier. Mistaking a human for a machine paywalls a reader
and breaks the whole promise; mistaking a machine for a human just misses a
fraction of a cent. So
agentDetectis tuned to favor humans, and declared intent (an agent that says it'll pay) is trusted over fragile user-agent sniffing. - No secrets in the repo. Credentials live in
.env(gitignored); only.env.exampleships.
The gate is built to sit on the public internet in front of a real site:
- Replay-proof payments. Every
402carries a fresh, HMAC-signed nonce bound to the price + payee; the agent echoes it in its payment and the gate spends it exactly once (nonce.ts). A capturedpayment-signaturecan't be replayed for a free read, and a cheap nonce can't be swapped onto a pricier resource. Mandatory in mock mode; live, the deposit-backed Circle Gateway settlement is the chain-level guarantor. - Rate limiting. A per-client token bucket
(
rateLimit.ts) caps request floods. Client identity is the socket peer IP;X-Forwarded-Foris trusted whenTRUST_PROXY=true(set it iff you run behind a proxy you control), and read from the RIGHT of the trail, so a client-supplied entry is never the key. Where there is no socket at all, as on a serverless adapter, the header is used regardless, because a caller able to forge it would have had a socket. - Header hygiene. The proxy strips hop-by-hop and internal
x-naulon-*/ payment headers before forwarding upstream, and re-derivesX-Forwarded-*/Hostitself so a client can't spoof its origin IP or host to the backend. - Validated trust boundary. Credits (which decide who gets paid) are parsed
through a strict schema before any wallet becomes a
payTo, so a malformed or hostile credits source is rejected, not settled (shared/credits.ts). - Dashboard exposure. The operator console shows wallets, earnings and traffic,
and it writes: the Content page rewrites
credits.json(who gets paid) and the Crawlers page rewrites the crawler policy (whether a bot is charged at all). Treat access to it as access to your payout configuration, not as a read-only leak. Every state-changing route is a POST behind a same-origin check, so a third party's page cannot drive it; a credential that reaches it can. Exposure is therefore deliberate. It binds127.0.0.1by default (private). Make it reachable, through a wide bind or a non-loopback name inDASHBOARD_ALLOWED_HOSTS, which is how a reverse proxy and a serverless deploy both announce themselves, and it requiresDASHBOARD_AUTH=user:pass(HTTP Basic), refusing to serve rather than leak. Failed sign-ins are metered, since Basic carries no lockout of its own.DASHBOARD_PUBLIC=trueserves only a masked earnings page. The gate (:8402) is built to face the internet; the console (:8403) is not. See docs/operating.md.
Hardening knobs (all optional, with safe defaults shown), add to .env:
TOLLGATE_SECRET= # HMAC secret for 402 nonces; ephemeral if unset (set for multi-instance)
NONCE_TTL_SECONDS=300 # how long a 402 nonce stays valid (also the replay window)
RATE_LIMIT_RPM=120 # sustained per-client requests/min; 0 disables
RATE_LIMIT_BURST=40 # short-burst allowance before 429
TRUST_PROXY=false # trust X-Forwarded-For for client IP (only behind your own proxy)
DASHBOARD_BIND=127.0.0.1 # dashboard interface; localhost-only by default (set 0.0.0.0 only behind your own auth)
PRIMARY_PAYEE_TIEBREAK=wallet # on-chain recipient on a top-share tie: by address (order-independent) or "input"Nonce + rate-limit state is in-process. For a multi-instance deployment, set a shared
TOLLGATE_SECRETand back both with a shared store (e.g. Redis).
See CONTRIBUTING.md for setup, conventions, and PR rules.
MIT.
Arc is a trademark of Circle Internet Group, Inc. and/or its affiliates. naulon is an independent project, not endorsed by or affiliated with Circle.