Skip to content
naulonappPublic

About

A toll you put in front of a site: AI agents pay a fraction of a cent in USDC to read or cite a page, and it goes straight to the author. Humans read free.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

353 Commits

Folders and files

Repository files navigation

🜉  naulon

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.

License: MIT Node Built on Arc Network


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.

How it works

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
Loading

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"]
Loading

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.

Quick start

Requires Node ≥ 22.

make install
make demo        # the whole loop offline: origin → toll → agent pays → settle

To 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 dev

It 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 proxy

make help lists every target.

Without cloning anything

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.

Check it actually tolls

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 doctor

selftest 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-naulon

The 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 paying agent

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 another
appraisal:
  · 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.

The operator console

npm run dashboard                         # http://localhost:8403

Your 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.

Settling payouts

npm run attribution        # one settlement pass over the ledger

Sub-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).

Use it with your own site

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_URL at your CMS (it serves /credits/:slug), or ship a static credits.json. For anything custom, implement the one-method CreditsResolver interface 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/.

Webhooks (settlement notifications)

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.

What's here

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
Loading
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

Develop

npm run lint     # typecheck the whole workspace
npm test         # unit tests (attribution splits, dust-free settlement, …)
npm run tollgate # or: wayfarer · attribution · dashboard

Everything runs straight from TypeScript via tsx, with no build step while developing.

Going live on Arc

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 / .settle against the Arc GatewayWallet (0x0077777d7EBA…, network eip155: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.deposit once, 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_NETWORK picks the chain: arcTestnet (default), baseSepolia, or base. 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 in shared/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
Loading

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 }

Deploying in front of a live site

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.

Design notes

  • 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 agentDetect is 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.example ships.

Hardening

The gate is built to sit on the public internet in front of a real site:

  • Replay-proof payments. Every 402 carries 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 captured payment-signature can'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-For is trusted when TRUST_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-derives X-Forwarded-* / Host itself 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 binds 127.0.0.1 by default (private). Make it reachable, through a wide bind or a non-loopback name in DASHBOARD_ALLOWED_HOSTS, which is how a reverse proxy and a serverless deploy both announce themselves, and it requires DASHBOARD_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=true serves 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_SECRET and back both with a shared store (e.g. Redis).

Contributing

See CONTRIBUTING.md for setup, conventions, and PR rules.

License

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.

About

A toll you put in front of a site: AI agents pay a fraction of a cent in USDC to read or cite a page, and it goes straight to the author. Humans read free.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages