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.
| Method | Path | Returns |
|---|---|---|
| GET | /api/stats/sites | Your sites (owner only, not read-only tokens) |
| GET | /api/stats/dashboard?siteId=…&range=30d&model=first&mode=net | Every 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=first | Visitors, 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_roasRevenue, ad spend and return per channelinspect_unmasked_directWhat “Direct” traffic really was: in-app browsers, shared links, typed visitsget_ai_search_citationsVisits and revenue from ChatGPT, Perplexity, Claude, Gemini and Copilotget_buyer_journeysRecent buyers and the pages they saw before payingcompare_buyer_reading_timeReading 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.