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
- Open Claude Code in your project root:
claude - Paste the prompt below.
- 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 signin | Layer 1 (snippet) — handled by passive detection |
| OAuth (Google / GitHub / Apple) | Layer 2 — call /i/identify in your OAuth callback |
| Magic-link auth | Layer 2 — call /i/identify on link redemption |
| Stripe / Paddle subscription | Layer 2 — call /i/conversion in the webhook |
| B2B SaaS where companies matter | window.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.
