Connect Windrose to your agent
Windrose runs as a hosted MCP server. You add it once to your coding agent, then ask questions in plain language. The agent calls the right tool and gets a compact answer back, so you never leave your editor.
1. Add the server
Run this in your project. It registers Windrose over HTTP transport under the name windrose.
claude mcp add --transport http windrose https://windrose.sh/mcp
Claude Desktop / claude.ai: Settings → Connectors → Add custom connector → https://windrose.sh/mcp. Any other MCP client (Cursor, VS Code, Codex): add an HTTP server entry to its MCP config:
{ "mcpServers": { "windrose": { "type": "http", "url": "https://windrose.sh/mcp" } } }
2. Authenticate
On first call the agent opens a browser to sign in with GitHub and mint a token scoped to your sites. The token lives in your agent config, not in your codebase.
3. Ask
Ask in plain language, or call a tool directly. Every response is summarized and token efficient, built to fit an agent context window rather than a spreadsheet.
> get_ai_traffic({ site: "windrose.sh", period: "30d" }) crawlers 18,340 ClaudeBot, GPTBot, PerplexityBot referrals 602 chatgpt.com, perplexity.ai, claude.ai
4. Optional: install the skill
A thin skill that teaches your agent the workflows: weekly traffic review, anomaly triage, and the AEO action loop, plus the reading rules (crawlers vs referrals, capture honesty).
mkdir -p ~/.claude/skills/windrose-analytics && curl -s https://windrose.sh/skill.md -o ~/.claude/skills/windrose-analytics/SKILL.md
Available tools
Track conversions and revenue
The browser beacon exposes window.windrose.track. Send a named event for a conversion, or include a non-negative value and three-letter currency for revenue:
windrose.track('signup')
windrose.track('purchase', { value: 49.90, currency: 'EUR' })
get_ai_traffic attributes later same-day events to the first AI-referred landing for that visitor-day. Values are totaled separately by currency. This is directional attribution, not proof that the AI source caused the conversion.
Crawler rows and totals classify canonical bots as training, indexing, retrieval, active-agent, or unknown from their documented User-Agent behavior. This purpose is inferred. IP verification confirms vendor identity, not the intent of an individual request.
Capture server-side crawlers (GPTBot, ClaudeBot, and friends)
Windrose's JavaScript beacon (a.js) only sees crawlers that run JavaScript. Most AI crawlers (GPTBot, ClaudeBot, PerplexityBot, and friends) just fetch your HTML and leave, so they never show up in a JS-only tracker, exactly the traffic you want to measure. Drop one small piece of middleware into your app and Windrose captures recognized AI-crawler hits server-side. Integrations that observe the final response also include its HTTP status, surfacing fixable crawl errors such as a 404 on /docs.
The middleware forwards bot hits to POST https://windrose.sh/api/collect/server, fire-and-forget, so it never blocks your response. Create a site-specific ingest key in the Windrose dashboard (or ask your agent to call create_server_ingest_key), save it as WINDROSE_INGEST_KEY, and send it as a bearer token. Windrose stores only its SHA-256 hash; rotating the key immediately invalidates the old one. Only recognized AI-crawler User-Agents are retained, and crawler IPs are used transiently for verification.
The snippets also forward the crawler's source IP. A User-Agent is a claim anyone can make; Windrose checks the IP against the claimed vendor's published ranges (OpenAI, Anthropic, Perplexity, Google) and records a verdict: verified, spoof suspect, or unverifiable. The IP itself is used transiently for that check and never stored.
Next.js (App Router, edge middleware) middleware.ts
// middleware.ts
import { NextResponse, type NextRequest, type NextFetchEvent } from 'next/server'
// config (SITE defaults to the request host when left empty):
const WINDROSE_ENDPOINT = 'https://windrose.sh/api/collect/server'
const WINDROSE_INGEST_KEY = process.env.WINDROSE_INGEST_KEY ?? ''
const SITE = '' // leave empty to default to the request host
const BOT_RE =
/GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Bytespider|CCBot|cohere-ai|Meta-ExternalAgent|Meta-ExternalFetcher|DuckAssistBot/i
export function middleware(request: NextRequest, event: NextFetchEvent) {
const ua = request.headers.get('user-agent') ?? ''
if (BOT_RE.test(ua)) {
const url = new URL(request.url)
const payload = {
site: SITE || url.host,
path: url.pathname,
ua, // the crawler's UA, from the incoming request
referrer: request.headers.get('referer'),
ip: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? null, // for crawler IP verification; never stored
ts: new Date().toISOString(),
}
// fire-and-forget: never block the response, always swallow errors
event.waitUntil(
fetch(WINDROSE_ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${WINDROSE_INGEST_KEY}` },
body: JSON.stringify(payload),
}).catch(() => {}),
)
}
return NextResponse.next()
}
// run on real page/content routes, skip static assets
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
Cloudflare Workers (reads the origin status, works in front of any origin)
// src/worker.ts (or the module entry configured in wrangler.toml)
// config (SITE defaults to the request host when left empty):
const WINDROSE_ENDPOINT = 'https://windrose.sh/api/collect/server'
const SITE = '' // leave empty to default to the request host
const BOT_RE =
/GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Bytespider|CCBot|cohere-ai|Meta-ExternalAgent|Meta-ExternalFetcher|DuckAssistBot/i
type Env = { WINDROSE_INGEST_KEY: string }
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// pass through to origin (replace with your own handler if this Worker IS the app)
const response = await fetch(request)
const ua = request.headers.get('user-agent') ?? ''
if (BOT_RE.test(ua)) {
const url = new URL(request.url)
const payload = {
site: SITE || url.host,
path: url.pathname,
ua, // the crawler's UA, from the incoming request
status: response.status, // final origin status
referrer: request.headers.get('referer'),
ip: request.headers.get('cf-connecting-ip'), // for crawler IP verification; never stored
ts: new Date().toISOString(),
}
// fire-and-forget: waitUntil lets it finish after the response is sent, catch swallows errors
ctx.waitUntil(
fetch(WINDROSE_ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${env.WINDROSE_INGEST_KEY}` },
body: JSON.stringify(payload),
}).catch(() => {}),
)
}
return response
},
}
Same one-file pattern for the other frameworks:
Verify it in one line. Hit any page as a crawler with curl -A GPTBot https://yoursite.com/, then open the AI traffic panel in your dashboard and look for the hit.
Record deployments from CI
Annotate each deploy so Windrose can compare equal pre/post evidence windows around it. CI systems post to POST https://windrose.sh/api/collect/deployment with the same WINDROSE_INGEST_KEY bearer token the middleware uses. The commit SHA makes a good idempotency_key: retries with identical content are deduped, reusing it with different content returns a 409.
curl -fsS -X POST https://windrose.sh/api/collect/deployment \
-H "content-type: application/json" \
-H "authorization: Bearer $WINDROSE_INGEST_KEY" \
-d '{"site":"example.com","idempotency_key":"'"$GITHUB_SHA"'","revision":"'"$GITHUB_SHA"'","environment":"production"}'
The full contract, a ready-to-paste GitHub Actions workflow, and the error table are in the CI deployments guide. Annotations are directional evidence, not causal proof.
Windrose's analytics tracker is cookieless and data-minimizing: raw visitor IPs are processed transiently, never stored in the analytics dataset, and the daily visitor identifier rotates every 24 hours. Your site's disclosure and lawful-basis requirements still depend on its setup and jurisdiction. The Privacy Kit documents the exact data flow and provides a copyable disclosure template.