NanoTarget docs
Portal
Overview

NanoTarget

Your customers sign in, then hand the session to an AI assistant. NanoTarget tells your server when that has happened and lets every endpoint decide what the assistant may see: allow, mask, step up or block — with a passkey that gives the session back to the person.

Inside the sessionBot management works at the door, before login. NanoTarget works after it, where the agent actually operates.
Decided on your serverThe decision is made in your process, next to your data. Nothing about it is delegated to the browser.
Yours to runRuns on your server. Open SDK, readable engine. Policy is your JSON, masking is your function, storage is your database.
New here? Quickstart gets a protected endpoint working in about ten minutes; How it works explains the model behind it.
Start

Concepts

Five words carry the whole system. Everything else is detail.

Session
One signed-in browser, across its tabs. You map a request to it with identify(req) — your own user or session id — or let a first-party cookie do it.
Resource
A named thing worth protecting: balance.read, customer.read, report.export, transfer.create. Resources are yours to name; they are the unit a policy talks about.
Actor
Who the engine believes is acting in the session right now: human-like, agent, or unknown. Unknown is its own answer — never a synonym for human.
Decision
What the endpoint does for this actor on this resource: allow, mask, step_up, block. Every decision is recorded.
Policy
A JSON file you own that maps resources to decisions, plus an enforcement mode: observe records what would have happened, enforce acts on it.
Start

How it works

An agent operating a signed-in session is indistinguishable from the customer at the network layer: same cookies, same address, same browser. So NanoTarget does not try to read the request. It reads the session.

Two channels

A
The page reports what it observed

A small first-party script watches the page for signs that something other than a person is operating it, and for evidence that a person is. It posts observations to your server, on your origin, as metadata. It makes no decisions and holds no secrets.

B
The endpoint asks for a decision

When a protected endpoint is called, the middleware asks the engine what it knows about this session. The answer combines everything observed so far with whatever accompanied this particular action.

What the engine weighs

Evidence is grouped by how much it proves, and the strongest class present decides:

ClassWhat it meansStrength
Verified operatorThe request carries a valid signature from a declared agent operator.proof
Deterministic browser factsProperties of the environment that a normal browsing session does not produce.proof
Active-control indicatorsTraces an agent tool leaves in the page while it is driving it.strong
Behavioural evidenceHow the session's interactions were physically produced. Judged by a model trained on labelled human and agent sessions.strong
Environment tracesThe session runs inside an AI application, or an agent tool is installed. A person may well be driving.weak

NanoTarget uses standard web APIs on your own origin. It does not fingerprint people, does not track across sites, and does not reverse-engineer any vendor's code. The exact measurements inside each class are the part we keep to ourselves — see the FAQ.

Three rules that shape every answer

  1. Unknown is never human. Absence of evidence selects the onUnknown branch of your policy — which you can make as strict as you like. An agent cannot become a person by staying quiet.
  2. A person is recognised by positive evidence. Either the way their actions were physically produced, or a passkey. Claims from the page are never taken at face value.
  3. Attachment sticks. Once an agent has been seen in a session, the session stays that way until a person proves presence with WebAuthn — going quiet does not clear it.

Where enforcement happens

In your server process, before your handler runs. A blocked read never reaches serialization; a masked read is produced by your mask function on the server. The browser is never asked to keep a secret.

One thing does happen in the page: values already rendered before an agent appeared are redacted in place, because the server cannot recall what it already sent. That is damage control, not the security boundary. With the script removed, the server still answers exactly the same way.

Start

Quickstart

Node ≥ 22.13. Works with Express 4/5, Connect, a Next.js custom server, or plain node:http.

  1. Install
    npm i nanotarget

    One package. It depends on nanotarget-engine, which npm installs with it — you never add or import the engine yourself. Everything you use is under nanotarget/express and npx nanotarget.

  2. See what an agent could reach
    npx nanotarget scan .            # routes, sensitivity, a draft policy
    npx nanotarget scan . --proposal # the same, in plain language
  3. Wire the middleware
    import { nanotarget } from 'nanotarget/express';
    
    const nt = await nanotarget({
      secret: process.env.NT_SECRET,           // ≥ 32 bytes, stable across restarts
      policy: './nanotarget.policy.json',
      db: 'sqlite:./nanotarget.db',
      apiKey: process.env.NT_API_KEY,          // optional: report to the portal
      identify: (req) => req.session?.userId ?? null,
    });
    app.use(nt.middleware());
    app.get('/api/balance', nt.protect('balance.read'), (req, res) =>
      nt.send(req, res, balance, (b) => ({ ...b, amount: null })));
  4. Add the page script and mark what is sensitive
    <script src="/nanotarget/sdk.js"></script>
    
    // call protected endpoints through the SDK
    const r = await NanoTarget.fetch('/api/balance');
    
    // rendered values that must disappear if an agent attaches
    <div data-nt-sensitive="full">$4,939.10</div>
  5. Verify
    npx nanotarget verify http://localhost:3000 /api/balance
Let an agent do it. The npm README is written as a protocol for coding agents: give it the link and say “install this”. It scans the app, proposes what to gate, asks you nine decisions and wires everything in.
Build

Policy reference

One JSON file. Unlisted resources are unprotected — list them in your own report deliberately.

{
  "version": "bank-policy-1",
  "enforcement": "observe",
  "rules": [
    { "resource": "balance.read",
      "title": "Account balance",
      "onAgent":     "mask",
      "onArtifact":  "mask",
      "onUnknown":   "allow",
      "onHumanLike": "allow" },
    { "resource": "report.export",
      "onAgent": "block", "onArtifact": "step_up",
      "onUnknown": "step_up", "onHumanLike": "allow" }
  ]
}
FieldMeaning
enforcementobserve records the decision and lets the request through; enforce acts on it.
onAgentAn agent is operating the session.
onArtifactEnvironment only: an AI application or an installed tool, no proof anything is driving.
onUnknownNot enough evidence in either direction. Never treated as human.
onHumanLikePositive human evidence, or a passkey within the reclaim window.

A default worth starting from

Resource classAgentEnvironmentUnknown
Balances, statements, transaction listsmaskmaskallow
Personal / contact / KYC datamaskmaskallow
Documents, exports, downloadsblockstep_upallow
Transfers, payments, ordersblockstep_upstep_up
Deletes, permission and settings changesblockstep_upstep_up
Credentials, tokens, recovery datablockblockstep_up
Health / medicalblockmaskallow
Search over customer recordsmaskmaskallow
Build

Masks, blocks and step-up

A mask is a second projection of the same record, built on the server: the page still renders, the value is gone. Keep the shape and the types.

const maskMoney = (b) => ({ ...b, amount: null, currency: b.currency, masked: true });
const maskIban  = (v) => v.slice(0, 4) + ' •••• ' + v.slice(-4);
const maskRows  = (rows, cap = 20) => rows.slice(0, cap).map(maskRow);
  • Mask the join, not only the field. A masked name beside an untouched e-mail or record id re-identifies in one step.
  • Mask derived values too. Totals, chart series, CSV previews and aria-labels carry the number you just removed.
  • Cap volume. A masked list of ten thousand rows is still a customer-base dump.
  • Never mask an irreversible action. A half-executed transfer is worse than a refused one — those are step_up or block.
  • Don't leak through errors. A validation message that echoes the value defeats the mask.

Downloads

Protect the request that issues the link and bind the file to the decision, so a copied URL is dead:

app.post('/api/statement', nt.protect('report.export'), (req, res) =>
  res.json({ url: `/files/${req.nt.token()}` }));

app.get('/files/:token', async (req, res) => {
  const ok = await nt.engine.redeemToken(req.params.token, req.nt.session.id, 'report.export');
  if (!ok) return res.sendStatus(403);
  streamStatement(res);
});

Step-up and reclaim

A step_up answers 428 with a challenge; a block on an agent session answers 403 and, where a passkey is registered, offers the reclaim path. A successful WebAuthn user-verified assertion hands the session back to the person for a few minutes. Nothing in the page can grant that by itself.

Build

Server API

PackageLicenceWhat it is
nanotargetApache-2.0What you install and import: the Express/Connect middleware (nanotarget/express), the browser SDK it serves, the CLI (npx nanotarget) and the proof verifier.
nanotarget-engineBUSL-1.1Its dependency, installed automatically: detection, policy, audit, proofs. Production use granted. You do not import it.
import { nanotarget } from 'nanotarget/express';
const nt = await nanotarget(options);
Option
secretrequired, ≥ 32 bytes; signs tokens and derives the tenant. Keep it stable.
policypath to the JSON file, or the object.
dbsqlite:./file, memory, or a libSQL URL for multi-instance and serverless.
identify(req)your login id, or null. Never a constant.
apiKeyreport decisions to the portal. Omit and nothing leaves your server.
telemetryImmediatesend each report at once instead of every 3 s. Automatic on Vercel, AWS Lambda, Netlify and Azure Functions; set it on any other platform that suspends the process between requests.
basePath, cookie, secure, tenant, respond, webauthnReclaimdefaults are sensible: /nanotarget, nt_sid, auto, default, true, true.
Method
nt.middleware()Mount before your routes. Serves the page script and its API under basePath.
nt.protect(resource, { respond })Decides, then answers 403/428 itself — or sets req.nt and calls next() when respond: false.
nt.send(req, res, full, mask)Sends full, or mask(full) when the decision says mask.
req.nt{ decision, masked, blocked, stepUp, actor, reasonCodes, session, token(), proof }. proof is the signed record of this decision — server-side only.
nt.reloadPolicy()Re-read the policy file without a restart.
nt.telemetry?.flush()Deliver queued reports before exit.
nt.proofBundle(sessionId)Every signed decision for one session, with the key that verifies them — the file you hand an auditor.
nt.proofFor(decisionId) · nt.verifyProof(jws) · nt.proofKeys()One proof by decision id; check a proof; the public key set.
Build

Browser SDK

Served from your own origin at <basePath>/sdk.js. It reports observations and redacts sealed elements; it never decides anything.

Member
NanoTarget.fetch(url, init)Same signature as fetch. Use it for protected endpoints so the action's evidence reaches the server.
NanoTarget.sessionHeaders()For axios/ky interceptors when you cannot swap fetch.
NanoTarget.onAssessment(fn)Called when the session's state changes — update your own UI.
NanoTarget.unseal(proof)Reveals sealed elements. Accepts only a server-issued reclaim proof.
data-nt-sensitive="full" | "masked"Marks rendered values. full elements are redacted the moment an agent appears; the document event nt:sealed fires once.

CSP: script-src 'self' and connect-src 'self' are enough.

Build

HTTP endpoints

Mounted by nt.middleware() under basePath (default /nanotarget):

GET /sdk.js — the page script
POST /signals — observations from the page
GET /connection — the session's current state, for your own UI
GET /session — session id and flags
POST /step-up — answer a challenge
POST /webauthn/register/options · /register · /assert/options · /assert — passkeys
GET /proof-keys — the public key that verifies your decision proofs (JWK Set)
GET /health — policy version, reporter state, integration warnings (503 while one stands)

Protected responses carry X-NT-Decision and X-NT-Policy, and nt.send adds a small _nt summary to the body so your front end can explain itself.

Operate

Portal & telemetry

Create a key in the portal and pass it as apiKey. The middleware then reports each decision in the background: batched, off the request path, dropped rather than blocking if the portal is unreachable, and switched off for good if the key is rejected.

The portal answers the question nobody can answer today: what share of your signed-in sessions had an AI agent in them — plus decisions over time, which agent products were seen, which resources they reached for, and a live log.

What a report contains

SentNever sent
A hash of the session id, the resource name, the decision, the actor, the session state, detected tool names, reason codes, enforcement mode, engine version, and the signed proof of the decision (the same fields, signed)Request or response payloads, customer identities, e-mail addresses, IP addresses, URLs, page text, form values

Decision proofs

Every decision is signed on your server the moment it is made, with a key derived from your secret. The proof states what was decided and why — resource, decision, actor, reason codes, policy and engine versions, whether data was delivered — and the decision's place in the tamper-evident audit chain. It carries a digest of the session, never the session id, and nothing the person typed or saw.

Your customers see nothing: no header, no field, no extra request. The proof travels with the telemetry report, the portal checks the signature on arrival, and each verified decision shows a ✓ in Activity. Export proofs downloads every signed decision in range — or click a ✓ for one session — as a single file an auditor can check without trusting us, you, or the database:

npx nanotarget verify-proof proofs.json --keys https://your-app.example/nanotarget/proof-keys

Auditor report, next to Export proofs, opens every signed decision in range as a printable page: how many verified, what was decided, who was acting, and the steps to check it independently. Save it as PDF and hand it to compliance together with the JSON file.

Proofs are standard compact JWS (EdDSA / Ed25519), so any JOSE library verifies them too. Pass --keys from your own site for an independent check; the keys inside a bundle prove it is consistent, your site proves where it came from.

People blocked

Activity opens with the number a bank asks first: how many people were blocked. It counts decisions the engine itself judged human that a rule still blocked or masked, plus gated decisions you graded wrong. It should stay at 0. Next to it: sessions judged human, and how often a person was asked to confirm with a passkey instead of being blocked.

Integration check

The Integration page shows four lights for the selected key, built only from what the key already reported: the server is reporting, pages send browser signals, signatures verify, and the policy is enforcing. Each warning names the fix. To test end to end, run npx nanotarget verify <your-app> <protected-path> from your own machine; the decision appears within seconds. NanoTarget never sends traffic to your site.

Weekly report

The last 7 days against the 7 before: sessions, the share with an AI agent, agents stopped, people blocked, passkey confirmations, which agent products are new on your site, what they reached for, and what changed in the agents themselves this month. Save it as PDF to forward to a security lead.

Policy builder

The Policy page lists the resources your middleware has reported and lets you pick the protection for each in plain words: open, masked for agents, block agents, block agents with a passkey when unsure, or a passkey for everyone. It writes the nanotarget.policy.json your server reads, in observe or enforce mode. Paste your current file to edit it. Nothing changes for your users until the server loads the new file.

Grading decisions

Every decision in Activity has two marks: right, or wrong. A gated decision marked wrong is a false stop (a person was masked or blocked); an allow marked wrong is a miss. The counts appear on the Activity page for the selected range. This is the only ground truth a detector gets from the field, and the reason to grade a few decisions a week.

Health

GET <basePath>/health on your server answers with the policy version, the reporter's state and any integration warning — most importantly when identify() returns the same value for many different visitors, which would merge them into one session. It returns 503 while a warning stands, so your monitoring notices.

Key lifecycle

A key can be rotated — the same key keeps its name, environment and history while a new secret is issued and the old one stops working at once — or revoked, which stops its servers reporting but leaves everything it already reported in place. A revoked key can then be deleted, and its decisions go with it. The decision log exports to CSV for whatever range is on screen.

Operate

Management API

A management key (nt_admin_…, created in the portal under Settings) administers an account over HTTP, so a coding agent or a CI job can set NanoTarget up without anyone opening the portal.

curl -H "Authorization: Bearer $NT_ADMIN" https://nanotarget-mvp.vercel.app/api/v1/manage/me
GET /api/v1/manage/me — the account, and this list
GET /api/v1/manage/keys — list project keys
POST /api/v1/manage/keys — {name, env, expiresInDays} → the raw key, once
POST /api/v1/manage/keys/:id/rotate — a new secret for the same key → the raw key, once
DELETE /api/v1/manage/keys/:id — revoke
GET /api/v1/manage/overview?range=7d — usage across every key
GET /api/v1/manage/stats?key=:id&range=7d — one key in depth
GET /api/v1/manage/events?key=:id&range=7d&before=:id — the decision log, paged
GET /api/v1/manage/proofs?key=:id&range=30d — signed decision proofs and the keys that verify them
GET /api/v1/manage/health?key=:id&range=7d — integration check and the people-blocked counter
GET /api/v1/manage/weekly?key=:id — the weekly report as data
POST /api/v1/manage/feedback — {key, id, verdict} grade one reported decision

A management key can create and revoke project keys. Treat it like a password and revoke it when the job is done.

Operate

Rollout

  1. Observe. Ship with "enforcement": "observe". Nothing is blocked; every decision is recorded with what would have happened.
  2. Read the log for a week or two. The portal shows how much agent traffic you actually have and which resources it reaches for.
  3. Tighten the policy where the log says it matters, then flip one word to enforce.
  4. Give people the way back. Register passkeys so a blocked person can reclaim their own session instead of calling support.
Operate

Privacy & data

  • The page script is first-party, served from your origin, and posts only to your origin.
  • It reports metadata about how the session behaves — never page text, form values, key identities or the content of what is displayed.
  • No cross-site identifiers, no third-party requests, no device fingerprinting for tracking purposes.
  • Decisions and their evidence live in your database. Telemetry to the portal is opt-in per key and carries the fields listed above.
  • Decisions are written to a hash-chained log, so an auditor can verify afterwards that the record was not edited.

For a security or legal review there is a one-page summary at /trust: what stays on your server, what leaves when you opt in, and what is never collected.

More

Limits & honest caveats

A security product that claims no limits is selling something. Ours:

  • Server-to-server traffic is out of scope. API keys used outside a browser session carry no session evidence; they belong to your existing API security.
  • Clients without the page script — mobile apps, partner integrations — arrive as unknown. Decide what that branch should do.
  • A person can still relay data by hand. No session-layer control stops someone reading their screen aloud.
  • This is an adversarial field. Agent products change; so do we. The engine is versioned, decisions record the version that made them, and the model is retrained against new evasion classes.
  • Detection is evidence, not omniscience. When the engine does not know, it says unknown instead of guessing — and your policy decides what that deserves.
More

FAQ

Why don't you publish exactly how detection works?
Because the people it is designed to detect read documentation too. We publish the model — evidence classes, decision tiers, the rules that shape every answer — and the measured outcomes. The specific measurements inside each class stay in the product. Customers under NDA get the full technical description and can run their own evaluation against it.
Does this block legitimate agents?
Only where your policy says so. The point is control, not prohibition: many teams allow reads and gate exports and money movement. A verified, signed agent operator can be treated differently from an anonymous one.
What happens if the page script is removed or fails?
The session has no human evidence, so it is unknown — never human. Your policy's onUnknown branch decides. Enforcement itself is unaffected: it lives on the server.
Does it slow requests down?
The decision is a local lookup and an evaluation against stored evidence — no outbound call on the request path. Portal telemetry is queued and sent in the background.
Where is the data stored?
In your database: node:sqlite on disk by default, or libSQL for multi-instance and serverless deployments.
What is the licence, and can we use it in production?
Yes. The browser SDK, the middleware, the CLI and the proof verifier (the nanotarget package) are Apache 2.0. The engine (nanotarget-engine) is Business Source License 1.1 with a production-use grant: run it in production, at any scale, to protect your own applications and the services you provide to your customers. The one use not granted is offering NanoTarget itself to third parties as a competing hosted or embedded product. Each version becomes Apache 2.0 four years after release.
Can we run it without any telemetry?
Yes. Leave apiKey unset and nothing leaves your network.
Questions we haven't answered here: open an issue, or read the agent protocol.
For AI agents

Agent protocol

NanoTarget is designed to be installed by a coding agent. Hand your agent one link and one sentence — install this — and it follows a written protocol instead of guessing.

Read https://nanotarget-mvp.vercel.app/docs/INTEGRATION-AGENT.md and install this into my app.

What the agent is told to do

  1. Analyse. Scan the codebase as an AI browser agent holding a customer's signed-in session, and report what such an agent could reach.
  2. Propose. Turn that into an exposure map: which resources are sensitive, and what should happen to each one.
  3. Ask. Put the decisions that are yours — not the agent's — in front of you, one at a time.
  4. Implement. Wire the middleware, tag the pages, write the policy file and every mask function.
  5. Verify. Run npx nanotarget verify and report what it left open.
The protocol file is plain Markdown, served for machines at /docs/INTEGRATION-AGENT.md. It is the same content an agent reads from the npm package as AGENTS.md.

Let it run your account too

A management key lets the agent create the project key, read usage and rotate secrets without you opening the portal — see the Management API.