{"_id":"@usenotra/geo","_rev":"6-bbf47b5b4c7f5fd3fade20d32cddafc6","name":"@usenotra/geo","dist-tags":{"beta":"0.1.0","latest":"0.5.0"},"versions":{"0.1.0-beta.1":{"name":"@usenotra/geo","version":"0.1.0-beta.1","keywords":["ai","geo","crawler","llms.txt","analytics","attribution","nextjs","nuxt","netlify","edge"],"license":"MIT","_id":"@usenotra/geo@0.1.0-beta.1","maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","dist":{"shasum":"078dfa6668c50c9ea3a8f7ca222967dda1312c3a","tarball":"https://registry.npmjs.org/@usenotra/geo/-/geo-0.1.0-beta.1.tgz","fileCount":20,"integrity":"sha512-83tKEINkquAkf2vwsY6xIXrCbgxJWVij5/WFaimmXlKpPzNIWU89Hl/LecjrDx+AJLEMsm8FUHFJFqt+u11yYA==","signatures":[{"sig":"MEQCICSKps0DDRGpOCu1CRvshPnurcObpiWCZWYdxYKChXHTAiB2eEkWyEFre2kTbiQ0pmStdK5yeMEnTjeGqmaZoRr6Mw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":114563},"type":"module","shasum":"078dfa6668c50c9ea3a8f7ca222967dda1312c3a","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./html":{"types":"./dist/html.d.ts","default":"./dist/html.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./nuxt":{"types":"./dist/nuxt.d.ts","default":"./dist/nuxt.js"},"./netlify":{"types":"./dist/netlify.d.ts","default":"./dist/netlify.js"},"./classify":{"types":"./dist/classify.d.ts","default":"./dist/classify.js"},"./markdown":{"types":"./dist/markdown.d.ts","default":"./dist/markdown.js"},"./signatures":{"types":"./dist/signatures.d.ts","default":"./dist/signatures.js"}},"private":false,"scripts":{"test":"bun test test","build":"tsup","check-types":"tsc --noEmit","prepublishOnly":"bun run build && bun run test"},"_npmUser":{"name":"dominikk","email":"dominik@rivo.gg"},"_integrity":"sha512-83tKEINkquAkf2vwsY6xIXrCbgxJWVij5/WFaimmXlKpPzNIWU89Hl/LecjrDx+AJLEMsm8FUHFJFqt+u11yYA==","repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"_npmVersion":"10.8.3","description":"Request capture SDK for AI traffic attribution","directories":{},"sideEffects":false,"_nodeVersion":"26.3.0","publishConfig":{"tag":"beta","access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","typescript":"5.9.2","@notra/typescript-config":"0.0.1"},"_npmOperationalInternal":{"tmp":"tmp/geo_0.1.0-beta.1_1787352340042_0.6467652650092566","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@usenotra/geo","version":"0.1.0","keywords":["ai","geo","crawler","llms.txt","analytics","attribution","nextjs","nuxt","netlify","edge"],"license":"MIT","_id":"@usenotra/geo@0.1.0","maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","bugs":{"url":"https://github.com/usenotra/notra/issues"},"dist":{"shasum":"70e5353cdbafd047ee24b1bc5a58e33f898dc2cb","tarball":"https://registry.npmjs.org/@usenotra/geo/-/geo-0.1.0.tgz","fileCount":20,"integrity":"sha512-2ED4xVHnES/+YW37vziiUX/qEsaI20MvIcq3UCAg7gpMHQ5umEuuhVB+tIIGMPZvYEY6sdKaF7nISx4UYf2Ang==","signatures":[{"sig":"MEQCIDtBX3AKWA/NrQaFeI3MAmpdVEaTVrVZF8CXSysEYIcbAiBkcKnf3qKUmpuQAW9S8gvyGoWdJK5x+DWo179q+tVSHA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":145983},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./html":{"types":"./dist/html.d.ts","default":"./dist/html.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./nuxt":{"types":"./dist/nuxt.d.ts","default":"./dist/nuxt.js"},"./netlify":{"types":"./dist/netlify.d.ts","default":"./dist/netlify.js"},"./classify":{"types":"./dist/classify.d.ts","default":"./dist/classify.js"},"./markdown":{"types":"./dist/markdown.d.ts","default":"./dist/markdown.js"},"./signatures":{"types":"./dist/signatures.d.ts","default":"./dist/signatures.js"}},"gitHead":"e983511331ee4d791581add207ae6165798b8945","private":false,"scripts":{"test":"bun test test","build":"tsup","check-types":"tsc --noEmit","prepublishOnly":"bun run build && bun run test"},"_npmUser":{"name":"dominikk","email":"dominik@rivo.gg"},"repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"_npmVersion":"11.6.2","description":"Request capture SDK for AI traffic attribution","directories":{},"sideEffects":false,"_nodeVersion":"24.11.1","publishConfig":{"tag":"beta","access":"public"},"_hasShrinkwrap":false,"readmeFilename":"README.md","devDependencies":{"tsup":"^8.5.1","typescript":"5.9.2","@notra/typescript-config":"workspace:*"},"_npmOperationalInternal":{"tmp":"tmp/geo_0.1.0_1787611010746_0.6649938156393145","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@usenotra/geo","version":"0.2.0","keywords":["ai","geo","crawler","llms.txt","analytics","attribution","nextjs","nuxt","netlify","edge","mcp","feedback","agents"],"license":"MIT","_id":"@usenotra/geo@0.2.0","maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","bugs":{"url":"https://github.com/usenotra/notra/issues"},"dist":{"shasum":"6a2b2d250ec53779b515294b0cc1214febbb6f17","tarball":"https://registry.npmjs.org/@usenotra/geo/-/geo-0.2.0.tgz","fileCount":22,"integrity":"sha512-Kf6JdNPNkQeCtzyYyIg7BRHu9DQITTjk5upVZlt1OJY9MOnnsPuxgJXqCyzZpk/VQTtsm1bYpPqqJe1BhwIatA==","signatures":[{"sig":"MEQCIBdNfQnq0IgYoEhGnJL4UQc/2x6S4WXGrpVBVB7XC1oKAiB063xUL4zU627s01G7BAdDESWKrHQQS5Cw8NZ5w/NlXA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":155118},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./html":{"types":"./dist/html.d.ts","default":"./dist/html.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./nuxt":{"types":"./dist/nuxt.d.ts","default":"./dist/nuxt.js"},"./netlify":{"types":"./dist/netlify.d.ts","default":"./dist/netlify.js"},"./classify":{"types":"./dist/classify.d.ts","default":"./dist/classify.js"},"./feedback":{"types":"./dist/feedback.d.ts","default":"./dist/feedback.js"},"./markdown":{"types":"./dist/markdown.d.ts","default":"./dist/markdown.js"},"./signatures":{"types":"./dist/signatures.d.ts","default":"./dist/signatures.js"}},"gitHead":"f64c93036216691da1f9528ee340cac9187f3c22","private":false,"scripts":{"test":"bun test test","build":"tsup","check-types":"tsc --noEmit","prepublishOnly":"bun run build && bun run test"},"_npmUser":{"name":"dominikk","email":"dominik@rivo.gg"},"repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"_npmVersion":"11.6.2","description":"Request capture SDK for AI traffic attribution and agent feedback collection","directories":{},"sideEffects":false,"_nodeVersion":"24.11.1","publishConfig":{"tag":"latest","access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.1.5","tsup":"^8.5.1","typescript":"5.9.2","@notra/typescript-config":"workspace:*"},"peerDependencies":{"zod":"^4.0.0"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/geo_0.2.0_1787731466625_0.22804498665585005","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@usenotra/geo","version":"0.3.0","keywords":["agents","ai","analytics","attribution","crawler","edge","feedback","geo","llms.txt","mcp","netlify","nextjs","nuxt"],"license":"MIT","_id":"@usenotra/geo@0.3.0","maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","bugs":{"url":"https://github.com/usenotra/notra/issues"},"dist":{"shasum":"a3cf1e1712da3cf0cef09b38892a65b52d8ba392","tarball":"https://registry.npmjs.org/@usenotra/geo/-/geo-0.3.0.tgz","fileCount":22,"integrity":"sha512-8l4NKUXwtdoAqP8+ZfB95yjpjSpexBgewCsPYpUAUZ2wY/CFvIX8HHHl0GRh51+KsKe0+1deJdmZsTEqi4Qxjg==","signatures":[{"sig":"MEUCIERrMUkP5XfaJK8fRnx18xVHO4Mbu99Es8oL6pKnnxySAiEA1s4XAjbwAWKHnuqjDU2lnbG5HiiiktaZ9ZH1u0hpffo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":158830},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./html":{"types":"./dist/html.d.ts","default":"./dist/html.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./nuxt":{"types":"./dist/nuxt.d.ts","default":"./dist/nuxt.js"},"./netlify":{"types":"./dist/netlify.d.ts","default":"./dist/netlify.js"},"./classify":{"types":"./dist/classify.d.ts","default":"./dist/classify.js"},"./feedback":{"types":"./dist/feedback.d.ts","default":"./dist/feedback.js"},"./markdown":{"types":"./dist/markdown.d.ts","default":"./dist/markdown.js"},"./signatures":{"types":"./dist/signatures.d.ts","default":"./dist/signatures.js"}},"private":false,"scripts":{"test":"bun test test","build":"tsup","check-types":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"dominikk","email":"dominik@rivo.gg"},"repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"_npmVersion":"11.6.2","description":"Request capture SDK for AI traffic attribution and agent feedback collection","directories":{},"sideEffects":false,"_nodeVersion":"24.11.1","publishConfig":{"tag":"latest","access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.1.5","tsup":"^8.5.1","typescript":"5.9.2","@notra/typescript-config":"workspace:*"},"peerDependencies":{"zod":"^4.0.0"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/geo_0.3.0_1788005048463_0.7162417128136158","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@usenotra/geo","version":"0.4.0","keywords":["agents","ai","analytics","attribution","crawler","edge","feedback","geo","llms.txt","mcp","netlify","nextjs","nuxt","tanstack"],"license":"MIT","_id":"@usenotra/geo@0.4.0","maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","bugs":{"url":"https://github.com/usenotra/notra/issues"},"dist":{"shasum":"9b282c27546b882b1093873d8759c00ca555d9fc","tarball":"https://registry.npmjs.org/@usenotra/geo/-/geo-0.4.0.tgz","fileCount":24,"integrity":"sha512-M9D1+05BgmhcnkMMWysNBcn2NnJz5nvD7RV9d+BQCy9v01V78fy3QRLzDtGhlI1kJt6/co4Y8VS/D4JhtsZIbA==","signatures":[{"sig":"MEUCIQC0Pr4jF89/+QTGdWVZ1gctTvy5AM+scTbJ5fbWN0dkygIgXbr2sqyoUUTt9oycwgrmQe8QavohQ1aPhc6ZAvja8FM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDeVFzA+UweQdbZj26K/KxVbLyxMEmjkRZuz3YWsOvJ1wIhAKu/K0gKyZKNEspyeSjDd5vxnSyQqATkyXBK/0Z9Mtx0","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169319},"type":"module","_from":"file:/tmp/notra-geo-release/usenotra-geo-0.4.0.tgz","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./html":{"types":"./dist/html.d.ts","default":"./dist/html.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./nuxt":{"types":"./dist/nuxt.d.ts","default":"./dist/nuxt.js"},"./netlify":{"types":"./dist/netlify.d.ts","default":"./dist/netlify.js"},"./classify":{"types":"./dist/classify.d.ts","default":"./dist/classify.js"},"./feedback":{"types":"./dist/feedback.d.ts","default":"./dist/feedback.js"},"./markdown":{"types":"./dist/markdown.d.ts","default":"./dist/markdown.js"},"./tanstack":{"types":"./dist/tanstack.d.ts","default":"./dist/tanstack.js"},"./signatures":{"types":"./dist/signatures.d.ts","default":"./dist/signatures.js"}},"private":false,"scripts":{"test":"bun test tests","build":"tsup","check-types":"tsc --noEmit --incremental --tsBuildInfoFile .cache/typecheck.tsbuildinfo","prepublishOnly":"bun run build"},"_npmUser":{"name":"dominikk","email":"dominik@rivo.gg"},"_resolved":"/tmp/notra-geo-release/usenotra-geo-0.4.0.tgz","_integrity":"sha512-M9D1+05BgmhcnkMMWysNBcn2NnJz5nvD7RV9d+BQCy9v01V78fy3QRLzDtGhlI1kJt6/co4Y8VS/D4JhtsZIbA==","repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"_npmVersion":"11.6.2","description":"Request capture SDK for AI traffic attribution and agent feedback collection","directories":{},"sideEffects":false,"_nodeVersion":"24.11.1","publishConfig":{"tag":"latest","access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","tsup":"^8.5.1","typescript":"5.9.2","@tanstack/react-start":"1.168.51","@notra/typescript-config":"0.0.1"},"peerDependencies":{"zod":"^4.0.0"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/geo_0.4.0_1789666845554_0.16194010842445694","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"_id":"@usenotra/geo@0.5.0","bugs":{"url":"https://github.com/usenotra/notra/issues"},"dist":{"shasum":"a04a8b410c31c2e5a4195a255b85e5ce4985078f","tarball":"https://registry.npmjs.org/@usenotra/geo/-/geo-0.5.0.tgz","fileCount":24,"integrity":"sha512-SUjbORLQ444fkvqRIMW9JeN2sC2wU6/iAzr3VtJet69ACnhlyprsmb35BjBPgqYDDEE4F4TVv0nqkJyyqGu3kA==","signatures":[{"sig":"MEUCID3DfEbqVUvlBC/S78nW77+c8JJDeMiFgsdDBWkLtvHpAiEAoQfy9YRE9Ysjw7svdDsM9u+BUcIrSz8ucEj0G17dIs0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC7j+Q5SDCmxr0ZnrZpkfApRjBa6Rf1raGPPBRCg+bDTwIhAKrDtIyi54gQhOokkTNMmkr/yuqinSvwAsGtGLz1YmyJ"}],"unpackedSize":169859},"name":"@usenotra/geo","type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./html":{"types":"./dist/html.d.ts","default":"./dist/html.js"},"./next":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"./nuxt":{"types":"./dist/nuxt.d.ts","default":"./dist/nuxt.js"},"./netlify":{"types":"./dist/netlify.d.ts","default":"./dist/netlify.js"},"./classify":{"types":"./dist/classify.d.ts","default":"./dist/classify.js"},"./feedback":{"types":"./dist/feedback.d.ts","default":"./dist/feedback.js"},"./markdown":{"types":"./dist/markdown.d.ts","default":"./dist/markdown.js"},"./tanstack":{"types":"./dist/tanstack.d.ts","default":"./dist/tanstack.js"},"./signatures":{"types":"./dist/signatures.d.ts","default":"./dist/signatures.js"}},"license":"MIT","private":false,"scripts":{"test":"bun test tests","build":"tsup","check-types":"tsc --noEmit --incremental --tsBuildInfoFile .cache/typecheck.tsbuildinfo","prepublishOnly":"bun run build"},"version":"0.5.0","_npmUser":{"name":"dominikk","email":"dominik@rivo.gg"},"homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","keywords":["agents","ai","analytics","attribution","crawler","edge","feedback","geo","llms.txt","mcp","netlify","nextjs","nuxt","tanstack"],"repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"_npmVersion":"11.6.2","description":"Request capture SDK for AI traffic attribution and agent feedback collection","directories":{},"maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"sideEffects":false,"_nodeVersion":"24.11.1","publishConfig":{"tag":"latest","access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","tsup":"^8.5.1","typescript":"5.9.2","@tanstack/react-start":"1.168.51","@notra/typescript-config":"workspace:*"},"peerDependencies":{"zod":"^4.0.0"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/geo_0.5.0_1789693770337_0.5484333412191016"}}},"time":{"created":"2026-08-21T22:45:39.937Z","modified":"2026-09-18T01:09:30.671Z","0.1.0-beta.1":"2026-08-21T22:45:40.187Z","0.1.0":"2026-08-24T22:36:50.883Z","0.2.0":"2026-08-26T08:04:26.757Z","0.3.0":"2026-08-29T12:04:08.604Z","0.4.0":"2026-09-17T17:40:45.625Z","0.5.0":"2026-09-18T01:09:30.427Z"},"bugs":{"url":"https://github.com/usenotra/notra/issues"},"license":"MIT","homepage":"https://github.com/usenotra/notra/tree/main/packages/geo","keywords":["agents","ai","analytics","attribution","crawler","edge","feedback","geo","llms.txt","mcp","netlify","nextjs","nuxt","tanstack"],"repository":{"url":"git+https://github.com/usenotra/notra.git","type":"git","directory":"packages/geo"},"description":"Request capture SDK for AI traffic attribution and agent feedback collection","maintainers":[{"name":"dominikk","email":"dominik@rivo.gg"}],"readme":"# @usenotra/geo\n\nRequest capture SDK for AI traffic attribution. It captures every page request your\nsite serves and sends a neutral request envelope to Notra. All classification, crawler\nversus AI referral versus human, happens server side at ingest, so a stale local table\nnever costs you traffic. Requests classified as human are discarded at ingest and never\nstored. The core tracker ships no signature table. Only the optional\nNext.js `tagLinks` path bundles the matcher, because it has to decide at the edge which\nresponses to rewrite for AI agents.\n\nZero dependencies. No Node APIs in the core, so it runs on edge runtimes.\n\n## Install\n\n```bash\nbun add @usenotra/geo\n```\n\n## Next.js\n\n```ts\n// proxy.ts on Next.js 16, middleware.ts before that\nimport { createGeoProxy } from \"@usenotra/geo/next\";\nimport { NextResponse } from \"next/server\";\n\nconst geo = createGeoProxy({ token: process.env.NOTRA_GEO_TOKEN ?? \"\" });\n\nexport function proxy(request: Request, event: { waitUntil(p: Promise<unknown>): void }) {\n  geo(request, event);\n  return NextResponse.next();\n}\n```\n\n`geo(request, event)` never throws and never blocks the response. When an event with\n`waitUntil` is passed, the send is registered on it so it survives the response.\n\n## Nuxt and Nitro\n\n```ts\n// server/middleware/geo.ts\nimport { createGeoHandler } from \"@usenotra/geo/nuxt\";\n\nconst geo = createGeoHandler({ token: process.env.NOTRA_GEO_TOKEN ?? \"\" });\n\nexport default defineEventHandler(async (event) => {\n  await geo(event);\n});\n```\n\nThe handler accepts an H3 event with a `node.req`, or any object exposing a standard\nweb `request`. Geo headers are usually absent behind Nitro, so those fields stay\nundefined and ingest falls back to IP based lookup.\n\n## TanStack Start\n\n```ts\n// src/start.ts\nimport { createMiddleware, createStart } from \"@tanstack/react-start\";\nimport { createGeoMiddleware } from \"@usenotra/geo/tanstack\";\n\nconst geo = createMiddleware().server(createGeoMiddleware({\n  token: process.env.NOTRA_GEO_TOKEN!,\n  endpoint: \"https://app.usenotra.com\",\n}));\n\nexport const startInstance = createStart(() => ({\n  requestMiddleware: [geo],\n}));\n```\n\nThe middleware captures requests alongside your route handler and waits for the send before returning, so tracking completes on serverless hosts. It preserves the downstream response and errors. If `src/start.ts` already exists, add `geo` to its existing `requestMiddleware` array.\n\n## Netlify Edge Functions\n\n```ts\n// netlify/edge-functions/geo.ts\nimport { createGeoHandler } from \"@usenotra/geo/netlify\";\n\nconst geo = createGeoHandler({ token: Deno.env.get(\"NOTRA_GEO_TOKEN\") ?? \"\" });\n\nexport default (request: Request, context: Context) => {\n  geo(request, context);\n};\n\nexport const config = { path: \"/*\" };\n```\n\nNetlify's `context.geo` is mapped into the envelope and `context.waitUntil` keeps the\nsend alive.\n\n## Options\n\n| Option | Default | Meaning |\n| --- | --- | --- |\n| `token` | required | Per organization ingest token |\n| `endpoint` | `https://app.usenotra.com` | Ingest origin, the SDK appends `/api/geo/ingest` |\n| `exclude` | `[\"/api\"]` | Paths to skip. Pass `[]` to disable |\n| `sample` | `1` | Fraction of eligible requests to send, 0 to 1 |\n| `onError` | none | Called with anything that goes wrong. The SDK never throws |\n| `fetch` | global `fetch` | Injectable fetch, for tests |\n\n### What gets captured\n\nOnly `GET` requests that look like pages. Asset requests are skipped: anything under\n`/_next/`, `/_nuxt/`, `/_vercel/`, `/_astro/`, `/static/`, and any path ending in a\ncommon static extension such as `.css`, `.js`, `.png`, `.svg`, `.xml` or `.txt`.\n`llms.txt` and `llms-full.txt` are the exception: they are always captured.\n\n`exclude` entries can be:\n\n- a string, matching that path and everything below it, so `\"/api\"` also excludes\n  `/api/users`\n- a `RegExp`, tested against the pathname\n- a function `(request, url) => boolean`\n\nPassing `exclude: []` removes the default and captures every page path.\n\n## Payload\n\nEach capture POSTs this JSON to `${endpoint}/api/geo/ingest` with an\n`authorization: Bearer <token>` header, `keepalive`, and a 2 second timeout.\n\n```ts\ntype GeoRequestPayload = {\n  timestamp?: string;\n  method: string;\n  url: string;\n  ip?: string;\n  geo?: {\n    country?: string;\n    region?: string;\n    city?: string;\n    timezone?: string;\n    latitude?: string;\n    longitude?: string;\n  };\n  referer?: string;\n  userAgent?: string;\n  accept?: string;\n  acceptLanguage?: string;\n  requestId?: string;\n};\n```\n\n`ip` comes from the first entry of `x-forwarded-for`, then `x-real-ip`, then\n`x-vercel-forwarded-for`. `geo` comes from the `x-vercel-ip-*` headers with\n`cf-ipcountry` as a country fallback, or from the platform context on Netlify.\n`requestId` comes from `x-request-id` or `x-vercel-id`.\n\nNo request body, no cookies, and no headers beyond the ones listed above are ever\nread or sent.\n\n## Journey attribution for markdown\n\n`@usenotra/geo/markdown` tags the links you hand to an agent so every follow up\nrequest is attributed to the same crawl session. Links carry a short `ntr` id,\nand the ingest side stores it as the journey for that visit.\n\n```ts\nimport {\n  getJourneyId,\n  mintJourneyId,\n  tagMarkdownLinks,\n} from \"@usenotra/geo/markdown\";\n\nexport async function GET(request: Request) {\n  const markdown = await readMarkdown();\n  const journeyId = getJourneyId(request) ?? mintJourneyId();\n\n  return new Response(\n    tagMarkdownLinks(markdown, journeyId, { host: \"example.com\" }),\n    { headers: { \"content-type\": \"text/markdown; charset=utf-8\" } }\n  );\n}\n```\n\n- `mintJourneyId()` returns a new url safe id built from `crypto.getRandomValues`\n- `getJourneyId(request)` reads a valid `ntr` param from a `Request`, `URL` or\n  string and returns `null` when there is none\n- `tagMarkdownLinks(markdown, journeyId, options)` rewrites inline links and\n  reference definitions in place\n\nOnly same site targets are tagged: paths starting with `/`, and absolute urls\nwhose host matches `options.host`. Existing query strings are preserved\n(`/docs?a=1` becomes `/docs?a=1&ntr=<id>`), fragments stay at the end\n(`/docs?a=1#usage` becomes `/docs?a=1&ntr=<id>#usage`), and anchors, `mailto:`,\n`tel:`, image embeds, external hosts and links that already carry an `ntr` param\nare left untouched. Fenced code blocks and inline code spans are never rewritten.\n\nRequests captured by the tracker already carry the full url, so no tracker\nconfiguration is needed for journeys to show up.\n\n## One-switch tagging (Next.js)\n\nOn Next.js you do not have to touch every markdown route. Turn on `tagLinks` and the\nproxy tags markdown responses for AI agents on the way out.\n\n```ts\n// proxy.ts on Next.js 16, middleware.ts before that\nimport { createGeoProxy } from \"@usenotra/geo/next\";\nimport { NextResponse } from \"next/server\";\n\nconst geo = createGeoProxy({\n  token: process.env.NOTRA_GEO_TOKEN ?? \"\",\n  tagLinks: true,\n});\n\nexport async function proxy(request: Request, event: { waitUntil(p: Promise<unknown>): void }) {\n  const tagged = await geo(request, event);\n  if (tagged) {\n    return tagged;\n  }\n  return NextResponse.next();\n}\n```\n\n`tagLinks` accepts `true` for the defaults, or an object:\n\n| Field | Default | Meaning |\n| --- | --- | --- |\n| `host` | request hostname | Host treated as same site when tagging absolute urls |\n| `paths` | `.md`, `llms.txt`, `llms-full.txt` | Which routes are markdown shaped |\n| `html` | `false` | Also tag the anchors in HTML pages served to AI agents |\n\n`paths` entries use the same matching semantics as `exclude`: a string matches that\npath and everything below it, a `RegExp` is tested against the pathname.\n\n### Mechanics\n\nFor each request the proxy checks, in order:\n\n1. the loop guard header `x-notra-geo-tag` is absent\n2. the method is `GET`\n3. the user agent matches a known AI agent from the shipped signature table\n4. the pathname matches one of the `paths` matchers\n\nWhen all four hold, the proxy resolves the journey id from the incoming `ntr` param or\nmints a new one, then fetches the same url again with the original request headers plus\n`x-notra-geo-tag: 1`. That second request re-enters the proxy, sees the guard header and\nfalls straight through to your route, so there is no loop. The origin response is only\nrewritten when it is `ok` and its content type contains `text/markdown` or `text/plain`.\nAnything else falls through untouched and the proxy behaves exactly as it did before.\n\n### HTML pages\n\nWith `html: true` the same flow covers ordinary pages. When the pathname does not match\nthe markdown matchers, the proxy falls back to the HTML path: it applies the tracker's\nown filter first, so `/api`, everything in `exclude`, and every asset path or static\nextension are skipped and never refetched. The origin response is rewritten only when it\nis `ok` and its content type contains `text/html`. Markdown always wins on markdown\nshaped paths, so nothing about the existing behaviour changes.\n\n`tagHtmlLinks(html, journeyId, options)` is also exported on its own from\n`@usenotra/geo/html` for stacks without the Next.js proxy.\n\nWhat it rewrites, and only this: the `href` of `<a>` start tags, single or double\nquoted, when the target is a path starting with `/` or an absolute url whose host\nmatches `options.host`. Existing query strings and fragments survive, with the param\ninserted before the fragment, and `&amp;` encoded queries stay encoded.\n\nWhat it never touches:\n\n- `<link>` tags, so `rel=\"canonical\"`, stylesheets, preloads and alternates are left\n  exactly as the origin wrote them\n- `src` attributes on images, scripts, iframes and media\n- anything inside `<script>` or `<style>`, since the rewrite is scoped to `<a` start\n  tags\n- `mailto:`, `tel:`, `javascript:`, `data:` and fragment only hrefs\n- protocol relative urls pointing at another host, and any external host\n- urls that already carry an `ntr` param\n\nAn invalid journey id returns the input unchanged. Relative hrefs that do not start\nwith `/` are left alone rather than guessed at.\n\nBecause the whole path is gated on the AI agent user agent check, human visitors and\nsearch crawlers get the untouched origin response byte for byte. There is no cloaking\nof content, only an extra query param on internal links for agents, and no effect on\nyour CDN or browser caching for normal traffic.\n\nOn success the request is tracked as usual and the proxy returns a new `Response` with\nthe tagged body, the origin status and the origin headers minus `content-length`.\n\nHuman traffic never pays for this: the agent check happens before the origin fetch, so a\nnormal browser request goes through the untouched path.\n\nTagging needs no ingest token. With `token` empty the proxy still tags and simply sends\nno envelope.\n\n### Other stacks\n\n`tagMarkdownLinks` stays the way to tag per route on Nuxt, Netlify or anything else.\nCall it inside the handler that builds the markdown, as shown above.\n\n## Core API\n\n`@usenotra/geo` exports the pieces the framework adapters are built from:\n\n- `Tracker` — `new Tracker(options).track(request)` returns a promise that never\n  rejects\n- `serializeRequest(request)` — builds the envelope from a standard `Request`\n- `shouldTrackRequest(request, url, exclude?)` — the GET, asset and exclude filter\n- `sendRequestLog(payload, options)` — the fire and forget POST\n\n## Signature table\n\n`@usenotra/geo/signatures` and `@usenotra/geo/classify` still ship the sourced AI agent\nsignature table and its matcher. The tracker does not use them. The ingest side uses\nthem to classify each captured envelope server side, and the Next.js `tagLinks` proxy\nuses the matcher locally to decide which responses to tag. Every entry carries a\n`source` URL and a confidence of `verified`, `reported` or `heuristic`. See\n[SOURCES.md](./SOURCES.md), which also documents agents that are deliberately absent\nbecause they cannot be honestly detected by user agent, including Pi, ChatGPT Atlas,\n`Google-Extended` and `Applebot-Extended`.\n\nUser agent matching is spoofable. The `verification` field on each signature points at\nthe operator's published IP range list or reverse DNS method where one exists.\n\n## Agent feedback\n\n`@usenotra/geo/feedback` adds a `submit_feedback` tool to your MCP server so the agents using your product can report bugs, request features and leave praise in your Notra inbox. Copy your feedback URL from the Feedback page in your dashboard. It is unique to your organization and needs no token.\n\n```ts\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { registerFeedbackTool } from \"@usenotra/geo/feedback\";\n\nconst server = new McpServer({ name: \"acme\", version: \"1.0.0\" });\n\nregisterFeedbackTool(server, {\n  url: \"https://api.usenotra.com/v1/feedback/acme\",\n  productName: \"Acme\",\n});\n```\n\nOutside of MCP, `submitFeedback(input, { url })` posts a single entry and resolves with `{ id, deduplicated }`. The MCP helper needs `zod` (already required by the MCP SDK); the plain client has no dependencies.\n\n## License\n\nMIT. Copyright (c) 2026 Notra, Inc. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}