One snippet in your middleware.
Every AI visit, captured.
Next.js runs your code on every request, so it's the perfect place to measure AI agents. Drop in our middleware (proxy.ts on Next 16) and each request fires a background event to Arrivl, no impact on your render.
A single middleware file and an environment variable. Your AI coding agent can wire it in from our prompt.
Install guide
Install Next.js, start to finish
Use Next.js middleware to send every page request to Arrivl. The call is fired in the background, so it never blocks your response.
02Install guide
Setup
Add your environment variable
.env.local
ARRIVL_WEBSITE_KEY=ak_YOUR_WEBSITE_KEY
Add the middleware
Create or update
middleware.tsin your project root:middleware.ts
// middleware.ts (Next.js 15) or proxy.ts (Next.js 16) // Next 16 note: rename the function to `proxy` and the file to `proxy.ts`. import { NextRequest, NextResponse, NextFetchEvent } from 'next/server'; export function middleware(request: NextRequest, event: NextFetchEvent) { const { pathname } = request.nextUrl; // Never track your own API or static assets: the matcher below excludes // most of these, this is a second-layer guard. if (pathname.startsWith('/api/')) return NextResponse.next(); // Real client/bot IP. Prefer Cloudflare's CF-Connecting-IP (CF sets it to // the true client) so a CF-fronted site records the real bot IP, not the // Cloudflare edge IP; fall back to the leftmost X-Forwarded-For, then // X-Real-IP. Matches the CF Worker's precedence. const ip = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || ''; // intake requires a non-empty ip; if none resolved (rare), skip the send. if (!ip) return NextResponse.next(); // Externally-visible URL. `request.url` carries the raw Host header, and // behind a reverse proxy — Vercel, Fly, Railway, Render, an ALB, nginx, any // container platform — that Host is the INTERNAL address, frequently // localhost. Arrivl stores that hostname on every event and only marks the // install verified once it matches your real domain, so reporting the // internal one means the site never verifies. When the proxy tells us the // public host (leftmost value: proxies append, they don't replace), swap it // into the origin and keep the rest of the URL untouched. No proxy in front // → request.url is already right and is used as-is. const fwdHost = request.headers.get('x-forwarded-host')?.split(',')[0]?.trim(); const fwdProto = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim(); const trackedUrl = fwdHost ? request.url.replace(/^https?:\/\/[^/]+/, `${fwdProto || 'https'}://${fwdHost}`) : request.url; const params = new URLSearchParams({ url: trackedUrl, userAgent: request.headers.get('user-agent') || '', ref: request.headers.get('referer') || '', ip, websiteKey: process.env.ARRIVL_WEBSITE_KEY!, }); // event.waitUntil sends the request in the background WITHOUT blocking the // response. A bare un-awaited fetch is killed on Edge/serverless (Vercel, // Cloudflare, Netlify): the runtime freezes the function the instant the // response is returned, so the event never leaves. waitUntil keeps it alive, // on every platform (no @vercel/functions needed). event.waitUntil( fetch(`https://arrivl.ai/api/v1/intake/pageview?${params}`).catch(() => {}) ); return NextResponse.next(); } // Track AI-discovery paths (robots.txt, llms.txt, sitemap.xml, .well-known) // even though their extensions look static: these are the highest-signal // hits Arrivl captures. The catch-all below then excludes other .txt/.xml/.json. export const config = { matcher: [ '/robots.txt', '/llms.txt', '/llms-full.txt', '/sitemap.xml', '/ai.txt', '/sitemap-:path*', '/.well-known/:path*', '/((?!api|_next/static|_next/image|favicon.ico|manifest.webmanifest|.*\\.(?:png|jpg|jpeg|svg|gif|webp|ico|css|js|mjs|map|woff2?|ttf|otf|eot|txt|xml|json)$).*)', ], };Deploy
Deploy your app, then load any page on the live site so Arrivl receives its first event: that first real event is what flips your project to Connected (writing the code isn't enough). The middleware sends events in the background via
event.waitUntil: non-blocking, and it survives Edge/serverless, where a bare un-awaitedfetchis killed.
03Install guide
How it works
- 01Middleware runs on every page request (static assets excluded by matcher).
- 02Reads
User-Agent,Referer, and IP from the request. - 03Fires the GET in the background via
event.waitUntil: non-blocking, and it survives Edge/serverless (a bare un-awaited fetch is killed once the response is sent)..catch(() => {})keeps failures silent. - 04Returns immediately via
NextResponse.next(): the tracking call never blocks the response.
04Install guide
Verify
bash
curl "https://arrivl.ai/api/v1/intake/pageview\
?url=https://yoursite.com/test\
&userAgent=Mozilla/5.0%20(compatible;%20GPTBot/1.0)\
&ref=\
&ip=1.2.3.4\
&websiteKey=ak_YOUR_WEBSITE_KEY"
# Should return: {"ok": true}01Middleware not tracking visits
Make sure the file is at the project root (next to package.json). Check that ARRIVL_WEBSITE_KEY is set.
02What about Next.js 16?
Next.js 16 renamed middleware.ts to proxy.ts and the export to export function proxy(). The rest is the same.
03Will this slow down my site?
No. The fetch() runs in the background via event.waitUntil: the response returns immediately and failures are silently caught. waitUntil also keeps the event alive on Edge/serverless, where a bare un-awaited fetch would be killed before it sends.
Values shown as ak_YOUR_WEBSITE_KEY are placeholders. Sign in and the in-dashboard version of this guide fills in your real key and per-project values for you.
Next step