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.
Optional · enable passive identity detection
When enabled, the snippet looks for the visitor's email in non-httpOnly cookies (JWT payloads), localStorage / sessionStorage, window globals, and visible DOM — useful for OAuth / magic-link / SSO signins where there's no form submit to detect. It can occasionally bind the wrong identity (shared devices, support agents with their own JWTs visible), so it's opt-in rather than default. EU regulators have specifically flagged this pattern in past enforcement.
Add the attribute to your snippet to turn it on:
<script async src="https://eyepup.com/t/YOUR_PROJECT_TOKEN.js"
data-eyepup-passive-identify="true"></script>Form-submit identify (extension #14) still works without this — that one only fires when the visitor explicitly types an email into a form, which is always informed-consent territory.
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 the cookie set by the Eyepup
snippet (it carries the project token in the cookie name). 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 →Test that it works
Once you've wired the three calls into your codebase, come back here and run these. They fire real requests at the same endpoints with your actual token — pass means your backend will work the same way.
Sign in to run live tests against your integration with your real project token.
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).
* 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.
