Skip to main content
Glama
README.md
# Pulse

**Open-source, revenue-first analytics that runs on your own Cloudflare.**

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/everyai-com/pulse)

Pulse tells you which post, search, referrer or AI answer brought the people who paid you, and how
AI assistants use your MCP tools. It's a self-hosted, drop-in alternative to DataFast: one Worker,
one D1 database, one script tag. Most sites fit in Cloudflare's free plan.

- **Revenue attribution**: Stripe, Polar and Dodo webhooks. Every sale, renewal and refund is credited to the visit that brought the buyer, even weeks later.
- **Projects home**: every site you run on one screen, each with its own pulse line, live visitors, revenue and MCP calls.
- **MCP usage**: see which tools your MCP servers get called with, by which AI client, how fast, and whether they fail.
- **AI search visibility**: checks every page the way Googlebot sees it, where you rank in Brave for real buying queries, and whether Claude (which searches with Brave) cites you, with a verdict on what to fix.
- **AI crawlers**: see when GPTBot, ClaudeBot, PerplexityBot and friends read your pages, which most analytics can't, because crawlers don't run JavaScript.
- **Ask your AI**: Pulse is itself an MCP server, so Claude, ChatGPT or Cursor can answer "where did last week's revenue come from?", and with a write token, add projects and funnels.
- **Yours**: your Cloudflare account, your data, MIT licensed. No cookies needed if you don't want them.

## Features

| | |
|---|---|
| Traffic | Visitors, sessions, pageviews, bounce, engaged time, scroll depth; channels, referrers, UTM campaigns, pages (entry, exit), countries, cities, devices, browsers, OS. Click any row to filter everything. ChatGPT, Claude, Perplexity and Gemini get their own **AI** channel. |
| Revenue | Revenue, sales, average order, share attributed, refunds, revenue per visitor, per source, per page and per country. One-click Stripe history import. |
| Readable Direct | No-referrer traffic split into typed/bookmarked, shared links (WhatsApp, iMessage, Slack…), checkout returns and named in-app browsers (Instagram, Facebook, LinkedIn, X, TikTok). |
| Lifetime value | Customers grouped by the source that first brought them, with renewals and repeat buyers. |
| Funnels | Pages (with `*` wildcards), goals and payment as steps; drop-off, median time between steps, revenue from finishers. Saved per project. |
| Page flow | For any page or section (`/docs/*`): where visits came from right before it and went right after. Click through to follow a path. |
| Conversions by source | Visits per referrer, channel, utm_medium or utm_campaign, and how many completed any goal, a chosen goal, or paid. |
| Bots & AI crawlers | Hits by category (AI crawlers, search, link previews, SEO tools, monitoring) and by bot and page, from the script and from your server. Real Chrome browsers running inside cloud networks (scanners, agents) are counted as automated browsers, not visitors. |
| AI search | Weekly: every sitemap page audited as Googlebot (status, robots.txt, noindex, canonical, links between your sites), Brave index and rank for your buying queries, and Claude's answer with web search (searched? found you? cited you? who instead?). One-click checklist for Brave's submit form. |
| Segments | Save any set of filters ("AI traffic from the US on mobile") and switch to it in one click. |
| Goals | `pulse("signup")`, `data-pulse-goal`, scroll goals (`data-pulse-scroll`), conversion rate and revenue per goal. Outbound link clicks are automatic. |
| Realtime | Live map and list of who's on the site, pushed over a WebSocket the moment someone arrives; visitors per minute, sales in the last hour, and the full journey of any visitor. |
| Alerts | Telegram: every sale with its source, traffic spikes (a source at 4× its usual hour), daily digest. |
| Team | Invite people by link as admins or viewers (email + password, no third-party auth). Viewers see everything but secrets and change nothing. |
| API tokens | Named read or write tokens for each AI assistant or script, with last-used dates and one-click revoke. |
| Sharing | A read-only public dashboard link per project. |
| Workflow | ⌘K command palette, custom date ranges, per-project timezone, CSV export everywhere, light and dark themes, works on phones, and an in-app notice when a new version is out. |

### Coming from DataFast?

Swap the script tag and keep your code: `window.datafast("signup")`, `data-fast-goal`,
`data-fast-scroll`, and checkouts that send `datafast_visitor_id` in metadata all keep working.
Then point your payment webhooks at Pulse.

## Install: one click

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/everyai-com/pulse)

1. Click the button and sign in to Cloudflare (a free account is fine).
2. Pick two passwords when asked: `ADMIN_TOKEN` (your dashboard login) and `PULSE_MCP_KEY` (for AI assistants).
3. Click **Deploy**. Cloudflare copies Pulse to your GitHub, creates the database and deploys it.
4. Open your new `pulse.<you>.workers.dev` URL, sign in with `ADMIN_TOKEN`, add a project and paste the tag.

To use your own domain, add it under the Worker's **Settings → Domains & Routes** in Cloudflare.
Updates: pull changes from this repo into your copy; Cloudflare redeploys and runs new migrations.

<details><summary>Prefer the terminal?</summary>

```bash
git clone https://github.com/everyai-com/pulse && cd pulse
npm install
npx wrangler login
npm run setup                                     # or: npm run setup -- --domain analytics.example.com
```

`setup` creates the database, applies migrations, deploys, and generates `ADMIN_TOKEN` and
`PULSE_MCP_KEY` for you (saved in `pulse-credentials.txt`, which is gitignored).
Upgrade later with `git pull && npm run deploy`.
</details>

## Track a site

```html
<script defer data-website-id="my-app" data-domain="example.com" src="https://YOUR-PULSE-URL/js/script.js"></script>
```

Single-page apps need nothing extra. Options:

| Attribute | |
|---|---|
| `data-cookieless` | No cookie and no storage. The server derives a visitor hash from a daily-rotating secret + IP + user agent, so visitors can't be linked across days (checkout attribution is off). |
| `data-allowed-hostnames="app.example.com"` | Carry one visitor across your own domains. |
| `data-exclude="/admin*,/preview/*"` | Don't track these paths. |
| `data-hash` | Track `#/routes`. |
| `data-disable-outbound`, `data-disable-payments` | Turn off outbound-click tracking or payment-link tagging. |
| `data-allow-localhost` | Track localhost while developing. |

In the browser: `pulse("goal", props)`, `pulse.identify({ user_id, email, name })`,
`pulse("payment", { email | payment_id })`, `pulse.visitorId()`, `pulse.ignoreMe()`.

## Connect payments

In Settings → Payments, copy the webhook URL for Stripe, Polar or Dodo, add it in the provider, and
paste the signing secret back. Then pass the visitor id into checkout:

```js
// Stripe Checkout (server), with visitorId from window.pulse.visitorId() in the browser
await stripe.checkout.sessions.create({
  ...,
  client_reference_id: visitorId,
  metadata: { pulse_visitor_id: visitorId },
  subscription_data: { metadata: { pulse_visitor_id: visitorId } },
});
// Polar or Dodo: metadata: { pulse_visitor_id: visitorId }
```

Stripe Payment Links, Polar links and Dodo `/buy/` links need nothing: the script tags them on click.
Checkouts that can't carry metadata: call `pulse("payment", { email })` on the thank-you page.

## MCP tool-call analytics

For an MCP server on the same Cloudflare account, add a service binding and log after each response.
The `PulseIngest` entrypoint is only reachable through bindings, so there's no secret to manage and
nobody can post fake events:

```jsonc
// wrangler.jsonc of your MCP server
"services": [{ "binding": "PULSE", "service": "pulse", "entrypoint": "PulseIngest" }]
```

```ts
ctx.waitUntil(env.PULSE.track("my-mcp", {
  name: "mcp_call", path: "/mcp",
  props: { tool: params.name, client: "claude", status: res.status, ms: Date.now() - t0 },
}));
```

Anywhere else: `POST /api/track` with `Authorization: Bearer <project API key>` and
`{ "name": "mcp_call", "props": { "tool": "...", "client": "...", "ok": true, "ms": 120 } }`. Never send tool arguments.
MCP servers report failures inside HTTP 200 (a JSON-RPC `error` or `isError: true`), so send `ok: false` for those;
the MCP view shows failure rates and average latency per tool.

## AI search visibility

Claude's web search retrieves from Brave, and Brave won't crawl a page Googlebot can't, so Pulse checks
that chain for every project once a week:

1. **Crawl audit** (always on): every sitemap page fetched as Googlebot.
2. **Brave index and rank**: `npx wrangler secret put BRAVE_SEARCH_API_KEY` (free key at api-dashboard.search.brave.com).
3. **Claude citations**: `npx wrangler secret put ANTHROPIC_API_KEY`. Each check is one Claude Opus 5.5 answer with
   web search, so it costs money; `SEO_AI_WEEKLY_LIMIT` caps checks per week (default 60).

Add buying queries per project ("best X 2026", "X vs Y"), or let Pulse suggest them. Brave's submit form has
a CAPTCHA, so submission stays a one-click copy-and-open checklist.

If Pulse runs on the same Cloudflare zone as a site it audits, Cloudflare can route its requests differently
from public ones; failures seen only from inside the zone are shown as "unverified" rather than as problems.

## AI crawler analytics

AI crawlers fetch your pages without running JavaScript, so log them where the page is served.
From a Worker with the `PULSE` binding:

```ts
const ua = request.headers.get("user-agent") || "";
if (/bot|crawl|spider|GPTBot|ClaudeBot|Perplexity|Google-Extended/i.test(ua))
  ctx.waitUntil(env.PULSE.crawl("my-app", { ua, path: new URL(request.url).pathname, status: res.status }));
```

Anywhere else: `POST /api/crawl` with the project API key and `{ "ua": "...", "path": "/", "status": 200 }`.
Pulse classifies the bot (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, Googlebot, Bingbot, link previews, SEO tools, uptime monitors…).

## Let your AI read your analytics

Pulse serves an MCP server at `/mcp`. Create a token for each assistant under **Team & API tokens**
(read, or read + write), or use `PULSE_MCP_KEY`.

Read tools: `list_sites`, `site_report`, `breakdown`, `timeseries`, `revenue_report`, `pages_report`,
`get_paths`, `get_attribution`, `get_bot_stats`, `get_search_visibility`, `list_goals`, `realtime`, `lifetime_value`, `mcp_usage`,
`run_funnel`, `list_funnels`, `visitor_journey`, `get_tracking_snippet`.
Write tools (write tokens only): `create_site`, `save_funnel`, `delete_funnel`.

```bash
claude mcp add --transport http pulse https://YOUR-PULSE-URL/mcp --header "Authorization: Bearer <token>"
```

Cursor, VS Code, Windsurf and Claude Desktop take the same URL and header. Clients that can't send
headers (Claude.ai and ChatGPT custom connectors) can use `https://YOUR-PULSE-URL/mcp/<PULSE_MCP_KEY>`;
treat that URL like a password.

## Alerts

```bash
npx wrangler secret put TELEGRAM_BOT_TOKEN   # from @BotFather
npx wrangler secret put TELEGRAM_CHAT_ID     # default chat; each project can override it
```

## How it works

```
browser ── /js/script.js ──▶ POST /api/e ─┐
Stripe · Polar · Dodo ── /webhooks/* ─────┤
your Workers ── PULSE binding (track, crawl) ┤
                                          ▼
                                 Worker ──┬──▶ D1: projects, visitors, sessions, events, payments (system of record)
                                          └──▶ LiveSite Durable Object per project: last 30 min in SQLite
                                                 └── WebSocket push to open dashboards (hibernates when idle)
dashboard (/), share links, /mcp ◀── Worker      cron every 15 min: spikes, daily digest → Telegram
```

- Visits are classified at ingest (referrer, UTM, in-app user agent, landing page) and stored per session, so filters are cheap.
- The live view comes from a per-project Durable Object, so it's instant and doesn't touch D1; reports read D1.
- A payment is credited to the visitor's last non-direct session in the 30 days before it (else their first). Refunds and renewals inherit the original sale's credit; webhooks that arrive out of order are back-filled.
- Bots never count as visitors; they're stored separately and classified. IPs are never stored.
- Money is stored in minor units with its currency and summed as-is (no currency conversion).

**Why D1 and not a data lake?** Pulse fits in Cloudflare's free plan: one Worker, one D1 database and
one Durable Object class. D1 comfortably handles small and mid-size sites (up to tens of millions of
events per database). If you outgrow it, the events table is the only thing to move.

## Develop

```bash
cp .dev.vars.example .dev.vars    # local secrets
npm run dev                       # local D1 + dashboard on http://localhost:8787
npm run check                     # typecheck + script syntax
```

Schema changes: add the next `migrations/NNNN_name.sql`. `npm run deploy` applies pending migrations before deploying.

## Security

Report vulnerabilities privately to support@magicteams.ai. The dashboard escapes every
visitor-supplied string; admin APIs need `ADMIN_TOKEN`; webhook signatures are verified; public
share links expose only overview, pages and realtime for one project.

## License

MIT