Next.js agent traffic tutorial

Track Claude Code, Codex, and Cursor Requests in Next.js

You can track public requests that may come from workflows involving Claude Code, Codex, Cursor, and other coding agents. You usually cannot prove the exact product or model behind every request.

That distinction should shape the implementation. A Next.js server can record the method, public path, response, request headers, and trusted network evidence it receives. It cannot read the private prompt, tool selection, or product UI that caused the request. Some agents fetch through a search provider. Others run a generic command such as curl, and network access may be disabled entirely.

This tutorial uses the current public Apostl Pulse SDK App Router quickstart. The code is short. The useful part is building an evidence model that retains unknown instead of turning a weak User-Agent into a confident product badge.

Three coding workstations sending public requests into one Next.js server
Classification ladder

Record the request first, then bound the label

Evidence the server receives
  • Full User-Agent when present
  • Public path and response
  • Trusted proxy-derived address
Defensible report
  • Supported agent evidence
  • Likely machine activity
  • Unknown remains visible
A coding tool may fetch through curl, a search provider, or a browser. The public server does not see the private prompt or product UI upstream.

Define what “track a coding agent” means

At the server boundary, tracking means observing an eligible request and estimating whether it belongs to machine activity. It does not mean identifying a developer, employer, exact model, or task.

The limitation comes from how coding agents reach the web. Claude Code's CLI can run permitted shell tools. Codex cloud internet access is disabled by default and can be enabled with domain and HTTP-method restrictions. The same OpenAI documentation includes a security example where a Codex shell command reaches a destination with User-Agent: curl/8.5.0.

Cursor Agent has a web-search tool, but its public documentation does not define a stable destination-facing User-Agent. A request associated with a Cursor workflow therefore need not contain the word “Cursor.” This is an architectural limitation, not a claim about every Cursor request.

Use three reporting buckets: supported agent evidence, likely machine traffic, and unknown. Exact product labels should appear only when the request carries stable, documented evidence. Product names in a dashboard are classifications, not identity documents.

Put collection on a real Next.js server route

Browser analytics starts after client JavaScript runs. A coding agent may fetch Markdown, llms.txt, an OpenAPI document, or an API endpoint without executing any page script. Collection therefore belongs in the server runtime that handles the request.

Next.js App Router Route Handlers are public HTTP endpoints built on the Web Request and Response APIs. That makes a real Route Handler a clean place to observe a public agent-facing surface. A static export has no request-time server, so this pattern requires a deployment mode where Next.js receives the request.

Next.js 16 renamed Middleware to Proxy. The current Pulse README does not ask you to invent a global Proxy integration; it provides a confirmed withPulse() wrapper for a Route Handler. Use that published contract first. It is easier to verify and keeps the measured surface explicit.

Install Pulse and keep its key server-only

Install the package:

npm install @apostl-dev/pulse-sdk

Set both variables in the deployed server runtime:

APOSTL_PULSE_ENDPOINT=https://ingest.apostl.dev
APOSTL_PULSE_API_KEY=your_server_key

Keep the key outside NEXT_PUBLIC_, public diagnostics, rendered HTML, logs, and Client Components. The Pulse README makes the same boundary explicit because createPulse() reads the server environment directly.

Create lib/pulse.ts:

import 'server-only';
import { createPulse } from '@apostl-dev/pulse-sdk';

export const pulse = createPulse();

The server-only import gives Next.js a build-time guard against accidental client use. The API key still needs ordinary secret management in your deployment platform.

Wrap the public llms.txt Route Handler

Create or update app/llms.txt/route.ts with the current SDK quickstart:

import { withPulse } from '@apostl-dev/pulse-sdk/next';
import { pulse } from '../../lib/pulse';

export const GET = withPulse(
  pulse,
  async () => new Response('# Agent docs\n', {
    headers: { 'content-type': 'text/markdown; charset=utf-8' },
  }),
  () => ({ surface: 'llms', surfaceName: 'llms-index' }),
);

Wrap the route that actually serves your content. A fake llms.txt added solely to make an analytics chart move gives agents nothing useful. The response should point to accurate, current resources, and the deployed public URL should return the expected content type.

The wrapper observes the response after the handler runs. Pulse treats an exact /llms.txt visit as immediate evidence for an estimated agent journey. The same public SDK pattern can be applied to other supported Route Handlers, but each surface should have a real purpose and a stable name.

Understand what Pulse records and excludes

Pulse records the canonical origin and path for eligible public traffic. Query parameters, fragments, request bodies, cookies, and authorization headers are not sent. The SDK sends trusted client IP and full User-Agent data because those signals participate in journey grouping and classification.

Eligible public health and API GET and HEAD responses are included. Auth and account routes, assets, mutations, and 5xx responses are excluded. Most /api/* routes are ignored unless their public prefix is configured; /api/mcp and /api/turnstile-config are documented safe defaults.

This boundary keeps the dataset focused on public content demand. It also means Pulse is not a replacement for your access logs, error tracker, security telemetry, or authenticated product analytics. If a route returns 500, diagnose it in operational logs. If a signed-in account completes an integration, record that outcome in the product.

Proxy headers need the same care. Use cf-connecting-ip only when the origin accepts traffic through Cloudflare, and configure trust for the proxy or CDN you actually control. Trusting an arbitrary forwarded header lets a client manufacture the network signal used for grouping.

Read classification as evidence, not attribution

Pulse groups eligible requests with the same project, trusted IP, and full User-Agent until a 30-minute inactivity window expires. An exact /llms.txt request qualifies immediately. A generic non-browser client can qualify after it requests two distinct machine-readable surfaces within ten minutes, such as openapi.json and a Markdown page.

This catches a useful class of tool-driven research without requiring the client to advertise an agent name. It also explains why one request should not automatically become “Codex visited pricing.” The official Codex security example reaches the destination through curl. Claude Code can run shell tools, while Cursor exposes web search without documenting a stable destination-facing identifier. The server sees the delivered request, not the product UI upstream.

Keep raw request count, estimated journey count, page demand, and explicit product labels separate. A request can support a page-demand conclusion even when the source product stays unknown. That row is still useful: it tells you a machine-shaped client requested a public resource.

Prove the first event before reading the dashboard

A successful deployment is not proof that telemetry arrived. The SDK README provides a bounded verification chain:

  • Start the production build with both server environment variables present.
  • Request the real public /llms.txt route with the deployment's trusted client-IP header and a full User-Agent.
  • Flush once through a temporary private test hook or graceful shutdown.
  • Check pulse.diagnostics() for configured=true, accepted>=1, sent>=1, droppedDelivery=0, and lastError=null.

diagnostics() returns counters, not the API key, captured headers, IP addresses, or request URLs. Keep the diagnostics route protected and temporary because the counters are operational metadata. Remove or disable it after the test.

Record the deployed origin, path, request time, response status, and diagnostics result together. If the dashboard is empty later, that proof separates an ingestion problem from a filter or reporting-window problem.

Turn page demand into one concrete change

Review the public routes with the most estimated agent activity and compare them with the resources agents need to complete common developer tasks. Check whether each route returns useful content without client-only rendering, whether links resolve, and whether the machine-readable version matches the human page.

Choose one change per review: repair a broken schema link, add a missing Markdown representation, clarify an authentication example, or expose a stable llms.txt index. Watch the same routes after the change. A request shift is traffic evidence; it is not proof of task completion or conversion.

Explore Apostl Pulse to add server-side agent traffic analytics to Next.js. The useful outcome is a defensible map of public machine demand, including the requests that remain unknown, not a chart that pretends every generic client announced its name.

See which public routes agents actually request.

Add Pulse to a real server route, prove the first event, and keep weak identities in the unknown bucket.

Explore Apostl Pulse