Grosslytics

Docs

Set up Grosslytics.

One script tag for visits, one webhook for sales. Then every payment is credited to the channel that brought the buyer.

1. Install the tracker

Sign in at /app and add your site. You get a site ID such as gl_8b5e717a83ac. Paste this into the <head> of every page:

<script defer src="https://grosslytics.com/g.js" data-site-id="YOUR_SITE_ID"></script>

Self-hosting? Use your own domain in src. The tracker sends events to the same domain it was loaded from, at /api/event. To send them somewhere else, add data-api="https://your-api/api/event".

Events are only accepted from the site’s own domain, its subdomains, and localhost. The tracker records a pageview when the page loads, plus reading time and scroll depth when the visitor leaves.

Single-page apps: the tracker records the page a visit loads on. Client-side route changes (React Router, Next.js links) are not recorded as separate pageviews yet.

2. Track goals

Add an attribute to any button or link. A click records the goal:

<button data-gl-goal="signup">Create account</button>

data-fast-goal and data-goal work too, so buttons already tagged for DataFast keep working. From JavaScript:

window.grosslytics('signup', { plan: 'pro' });

window.datafast, window.databull and window.plausible point to the same function, so existing calls keep working after you swap the script.

3. Connect payments

In your payment provider, add a webhook pointing at the URL below. Then, in /app → Payments, paste the webhook’s signing secret. Unsigned webhooks, or ones with the wrong signature, are rejected.

https://grosslytics.com/api/webhooks/{provider}?siteId=YOUR_SITE_ID

To credit a sale to a channel, the payment has to carry the buyer’s visitor ID. On checkout links in your page, the tracker adds it for you. Anywhere else, read it in the browser and pass it to your checkout:

const visitorId = localStorage.getItem('gl_vid');

If there is no visitor ID, a returning buyer is still matched by email when they paid before, and otherwise the sale is counted as unattributed.

Stripe /stripe

Events: checkout.session.completed, invoice.paid, invoice_payment.paid, charge.refunded

Visitor ID: Payment Links and Checkout links on your page are tagged automatically (client_reference_id). With the Checkout API, pass client_reference_id or metadata.visitor_id. For renewals, put visitor_id in the subscription’s metadata.

Polar /polar

Events: order.paid

Visitor ID: Checkout links on your page are tagged automatically (reference_id). Or pass metadata.visitor_id.

Lemon Squeezy /lemonsqueezy

Events: order_created, subscription_payment_success

Visitor ID: Checkout links on your page are tagged automatically (checkout[custom][visitor_id]).

Paddle /paddle

Events: transaction.completed

Visitor ID: Pass customData: { visitor_id } when you open Paddle Checkout.

Razorpay /razorpay

Events: payment.captured

Visitor ID: Pass notes: { visitor_id } when you create the order or payment.

Currencies and refunds

Each site reports in one currency: USD, EUR, GBP or INR, chosen when you add the site. A payment in another currency is refused (HTTP 422), never converted or mixed in. A full Stripe refund removes the sale from revenue; partial refunds are not handled yet.

Net revenue subtracts each provider’s standard published fee, which is an estimate; your actual fee can differ.

4. Read the dashboard

Attribution: first touch credits the source that first brought the buyer, last touch the one right before they paid, and linear splits each sale 50/50 between the two.

Revenue: gross is what the buyer paid, net subtracts payment fees, and profit also subtracts the ad spend you enter for each source and day.

Sources: visits that other tools call “Direct” are named where possible: in-app browsers (Instagram, Facebook, LinkedIn, TikTok, Telegram, X), AI search, links with ?utm_source=, ?ref= or ?via=, and shared links versus typed addresses.

Also on the dashboard: live visitors and sales, each buyer’s journey, lifetime value by first source, sales by day and hour in your site’s timezone, chart annotations, and an optional public revenue page at /share/your-domain that never shows customer emails or journeys.

5. API

Create a read-only token in /app → API tokens. Each token reads one site and is shown once. Send it as a bearer token:

curl -H "Authorization: Bearer gl_pat_…" \
  "https://grosslytics.com/api/stats/dashboard?siteId=YOUR_SITE_ID&range=30d"

Read-only tokens can only make GET requests for their own site. The writes below need the owner’s API_TOKEN (self-hosting) or a signed-in dashboard session.

MethodPathReturns
GET/api/stats/sitesYour sites (owner only, not read-only tokens)
GET/api/stats/dashboard?siteId=…&range=30d&model=first&mode=netEvery dashboard number in one response. range: 24h, 7d, 30d, 90d. model: first, last, linear. mode: gross, net, profit. Filters: source, page, country, device.
GET/api/stats?siteId=…&range=30d&model=firstVisitors, sales and revenue per channel
GET/api/stats/journey/{visitorId}?siteId=…One visitor’s pages, goals and payments in order
POST/api/stats/ad-spend{ siteId, source, date: "YYYY-MM-DD", amountCents } sets one day’s spend for a source
POST/api/stats/annotations{ siteId, at: unix seconds, label } adds a marker to the chart

6. AI agents (MCP)

Grosslytics includes an MCP server, so Claude Code, Cursor or Windsurf can answer questions like “which channel earned the most per visitor this month?” Use a read-only API token. In Claude Code:

claude mcp add --transport http grosslytics \
  "https://grosslytics.com/api/mcp?site=YOUR_SITE_ID" \
  --header "Authorization: Bearer gl_pat_…"

Other clients take the same URL and header in their MCP settings (Streamable HTTP). Tools:

  • query_channel_roas Revenue, ad spend and return per channel
  • inspect_unmasked_direct What “Direct” traffic really was: in-app browsers, shared links, typed visits
  • get_ai_search_citations Visits and revenue from ChatGPT, Perplexity, Claude, Gemini and Copilot
  • get_buyer_journeys Recent buyers and the pages they saw before paying
  • compare_buyer_reading_time Reading time and scroll depth of buyers vs everyone else

7. Self-hosting

Grosslytics runs entirely in your own Cloudflare account: Workers, D1, Queues and a Durable Object. The free Workers plan is enough for most sites.

You need Bun, Node.js 18 or newer, and a domain on Cloudflare. From a copy of the code, one command sets everything up: it signs in to Cloudflare (you click Allow), creates the database and queues, deploys the API and dashboard to a hostname on your domain, and sets your dashboard password. Run it again to update.

git clone https://github.com/vidneat/grosslytics.git
cd grosslytics && bun install
bun run setup

The Lifetime license (opening soon) will do the same without the code: npx grosslytics install.

On first visit, a self-hosted dashboard asks for your API_TOKEN once and a password; after that you sign in with the password. Five wrong passwords lock sign-in for 15 minutes.

8. Privacy and bots

The tracker sets no cookies. It keeps a random visitor ID in the browser’s local storage and a session ID in session storage. Buyer emails are never stored in full: only a hash (to recognize repeat buyers) and a masked form such as j***@gmail.com for the dashboard.

Known bots, headless browsers and traffic from cloud data centers are dropped before they are counted. A visit whose browser identifies as an AI crawler, such as GPTBot, is recorded as an AI crawl instead of a visitor.

For AI assistants, a plain-text summary of this product is at /llms.txt.