Two layers of Eyepup integration

The snippet handles passive tracking out of the box — Layer 1, and most teams never need anything else. For OAuth, magic-link, server-side conversions, and explicit business events, drop in Layer 2 via Claude Code in 5 minutes.

Layer 1 — the snippet (default)

Paste this in your <head>. Done. The tracker captures sessions, autocaptures clicks, records pages, runs four layers of passive identity detection (cookies, localStorage, window globals, visible DOM), and ships everything to your dashboard.

<script async src="https://eyepup.com/t/YOUR_PROJECT_TOKEN.js"></script>

That's it. Visitors get profiled. Skip directly to the dashboard.

Layer 2 — deep integration via Claude Code

For OAuth, magic-link, server-side conversions, and any business event the browser can't see. Three new endpoints, one Claude Code prompt. The prompt below tells Claude how to wire everything into your codebase — auth flows, Stripe webhooks, custom events.

How to use

  1. Open Claude Code in your project root: claude
  2. Paste the prompt below.
  3. Approve Claude's diff. Test. Ship.

The prompt

You are integrating Eyepup analytics into this codebase. Eyepup is a privacy-first, AI-powered visitor analytics tool that watches every session and writes a per-visitor verdict.

The user already pasted the Eyepup snippet on their site. Your job is to add the Layer 2 deep integration: explicit identify calls + server-side event tracking. Keep changes minimal and idempotent — don't refactor unrelated code.

## Your task

1. Read the codebase. Identify:
   - Auth flow (signin, signup, OAuth callback, logout)
   - Payment / subscription webhooks (Stripe / Paddle / LemonSqueezy)
   - Key business events the team would care about (signup_completed,
     plan_upgraded, feature_used, etc.)

2. Add the EYEPUP_PROJECT_TOKEN env var to .env.example. The user will
   find their token on https://eyepup.com/sites — it's the same token
   already in their snippet URL (`/t/<TOKEN>.js`).

3. Wire the THREE deep-integration calls:

### A) On signin / signup success (server-side, in the auth callback)

```ts
await fetch("https://eyepup.com/i/identify", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    authorization: `Bearer ${process.env.EYEPUP_PROJECT_TOKEN}`,
  },
  body: JSON.stringify({
    distinct_id: anonymousIdFromCookie, // visitor's pre-auth id
    user_id: user.id,
    email: user.email,
    properties: { plan: user.plan, signup_at: user.createdAt },
  }),
}).catch(() => {});
```

The `distinct_id` is the visitor's pre-signin cookie value. Read it
from the request cookies — look for a cookie matching
`/^ph_phc_[a-z0-9]+_posthog$/`. Parse the JSON value and use
`distinct_id` from there. If the cookie isn't readable (httpOnly,
SSR), pass null — Eyepup falls back to passive detection.

### B) On Stripe / payment webhook (server-side)

```ts
// In the charge.succeeded / invoice.paid handler
await fetch("https://eyepup.com/i/conversion", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    authorization: `Bearer ${process.env.EYEPUP_PROJECT_TOKEN}`,
  },
  body: JSON.stringify({
    distinct_id: customer.metadata.eyepup_distinct_id ?? customer.email,
    name: "subscribed",
    amount: charge.amount / 100,
    currency: charge.currency.toUpperCase(),
    properties: { plan, interval, source: "stripe_webhook" },
  }),
}).catch(() => {});
```

### C) On any meaningful business event (server OR browser)

Server-side:
```ts
fetch("https://eyepup.com/i/event", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    authorization: `Bearer ${process.env.EYEPUP_PROJECT_TOKEN}`,
  },
  body: JSON.stringify({
    distinct_id: user.id,
    event: "feature_used",
    properties: { feature: "export_csv" },
  }),
}).catch(() => {});
```

Browser-side (the snippet exposes these as `window.eyepup.*`):
```ts
window.eyepup.identify(user.email, { plan: user.plan });
window.eyepup.capture("feature_used", { feature: "export_csv" });
```

## Constraints

- ALL fetch calls to eyepup.com MUST be wrapped in .catch(() => {}) —
  analytics MUST NEVER break the user's flow.
- Use `waitUntil()` (Vercel) or fire-and-forget on the runtime if
  available so the response isn't blocked on Eyepup.
- Don't log the project token to any error reporter.
- Don't add any new dependencies — these are fetch() calls.

## After you finish

Run the user's existing test suite. If it passes, summarize:
  1. Which auth flow you wired
  2. Which webhook handlers you hooked
  3. Any custom events you added
  4. Which env var to add to production (EYEPUP_PROJECT_TOKEN)
Download as .md →

The three Layer-2 endpoints

POST /i/identify

Stitches a pre-auth visitor to a post-auth identity. Call from your auth callback (server-side).

curl -X POST https://eyepup.com/i/identify \
  -H "Authorization: Bearer YOUR_PROJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "distinct_id": "anon-cookie-value",
    "user_id": "u_123",
    "email": "user@example.com",
    "properties": { "plan": "pro", "signup_at": "2026-05-02" }
  }'

POST /i/conversion

Typed conversion event. Boosts the visitor's heat score, triggers the first-conversion email, lights up the dashboard conversion charts. Call from Stripe / Paddle / LemonSqueezy webhooks.

curl -X POST https://eyepup.com/i/conversion \
  -H "Authorization: Bearer YOUR_PROJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "distinct_id": "u_123",
    "name": "subscribed",
    "amount": 49.00,
    "currency": "USD",
    "properties": { "plan": "pro", "interval": "month" }
  }'

POST /i/event

Generic server-side event capture. Anything the browser can't see — backend cron jobs, scheduled tasks, agentic workflows, internal admin actions.

curl -X POST https://eyepup.com/i/event \
  -H "Authorization: Bearer YOUR_PROJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "distinct_id": "u_123",
    "event": "feature_used",
    "properties": { "feature": "export_csv" }
  }'

Browser SDK (window.eyepup)

The snippet exposes a typed SDK on window.eyepup. Drop the type definitions into your project and call directly:

// Type definitions for the window.eyepup browser SDK.
// Drop into your project at: src/types/eyepup.d.ts

interface EyepupSDK {
  /** Tie the current visitor to an external identifier (email, user_id).
   *  PostHog-compatible: anonymous → identified visitors get auto-merged. */
  identify(distinctId: string, properties?: Record<string, unknown>): void;

  /** Capture a custom event. Use for business signal — signup_completed,
   *  feature_used, plan_upgraded — that's not auto-captured. */
  capture(eventName: string, properties?: Record<string, unknown>): void;

  /** Set persistent person-level properties without firing an event. */
  setPersonProperties(properties: Record<string, unknown>): void;

  /** Record a conversion with optional revenue. Boosts heat score and
   *  triggers the first-conversion email. */
  capture(
    eventName: "conversion",
    properties: {
      conversion_name: string;
      amount?: number;
      currency?: string;
      [k: string]: unknown;
    },
  ): void;

  /** Group analytics — for B2B accounts where you care about company-
   *  level behavior, not just individual visitors. */
  group(groupType: string, groupKey: string, properties?: Record<string, unknown>): void;

  /** Explicit logout — clears the visitor's identity and starts a fresh
   *  anonymous distinct_id. Call this in your client-side logout handler. */
  reset(): void;

  /** Read the current visitor's distinct_id (anonymous OR identified).
   *  Useful for passing to /i/identify from your auth callback. */
  get_distinct_id(): string;
}

declare global {
  interface Window {
    eyepup: EyepupSDK;
  }
}

export {};

When to use which

If you have…Use…
A normal email + password signinLayer 1 (snippet) — handled by passive detection
OAuth (Google / GitHub / Apple)Layer 2 — call /i/identify in your OAuth callback
Magic-link authLayer 2 — call /i/identify on link redemption
Stripe / Paddle subscriptionLayer 2 — call /i/conversion in the webhook
B2B SaaS where companies matterwindow.eyepup.group("company", "acme-inc")
Internal admin events you care about/i/event with custom event name

Need help wiring it? Open a Claude Code session in your repo, paste the prompt above, approve the diff. Ten minutes from decision to deployed.