# Lodd Documentation

Last updated: 2026-06-04

## Quick start

One prompt to set up: tell your coding agent "Add lodd.dev analytics to this project." It handles authentication, script embedding, and deployment. See [/llms.txt](/llms.txt) for the full agent setup flow.

For a tool-specific walkthrough, see the guides for [Claude Code](/blog/check-traffic-from-claude-code), [Cursor](/blog/web-analytics-cursor), and [Codex](/blog/web-analytics-codex-openai).

Lodd is deliberately read-only. It does not trigger A/B tests, roll back deployments, or mutate site behaviour. Coding agents already have those capabilities — they read analytics, decide what to change, then write the code. Lodd provides the signal; the agent closes the loop.

## Tracking script

The tracking script is a single `<script>` tag added to your HTML `<head>`:

```html
<script defer src="https://lodd.dev/tracking/v1.js"
  data-site-id="YOUR_SITE_ID"
  data-tracking-secret="YOUR_SECRET"></script>
```

This automatically tracks page views, session duration, SPA navigations, referrers, UTM parameters, device info, and page load time. No cookies. Country-only geo. Bots are detected and tagged.

## Custom events

Track any user action with `window.ca.track()`:

```js
window.ca.track("event_name", { key: "value" })
```

- `event_name` — string, max 100 characters
- `properties` — optional object, max 10KB when serialised

Events count towards your monthly event limit alongside page views.

### Revenue tracking

Attach revenue to any event by including `revenue` and `currency` in properties:

```js
window.ca.track("purchase", {
  revenue: 49.99,
  currency: "EUR",
  plan: "pro"
})
```

- `revenue` — number (extracted from properties, stored separately for aggregation)
- `currency` — 3-letter ISO 4217 code (e.g. "EUR", "USD", "GBP")

Revenue fields are automatically extracted from properties and stored in dedicated columns. They appear in `get_event_counts` as `total_revenue`, `avg_revenue`, and `revenue_currency`.

Other properties (like `plan` in the example above) are kept in the properties object as usual.

### Common patterns

**E-commerce funnel:**
```js
window.ca.track("add_to_cart", { product: "Widget", revenue: 29.99, currency: "USD" })
window.ca.track("checkout_start")
window.ca.track("purchase", { revenue: 29.99, currency: "USD", order_id: "ORD-123" })
```

Then query the funnel:
> "Show me the conversion funnel from add_to_cart to checkout_start to purchase"

**Scroll depth:**
```js
const tracked = new Set();
const observer = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting && !tracked.has(e.target.id)) {
      tracked.add(e.target.id);
      window.ca.track("scroll_depth", { depth: e.target.id });
    }
  });
});
["25", "50", "75", "100"].forEach(pct => {
  const el = document.getElementById(`scroll-${pct}`);
  if (el) observer.observe(el);
});
```

Place invisible markers in your content:
```html
<div id="scroll-25" style="height:0"></div>
<!-- ... content ... -->
<div id="scroll-50" style="height:0"></div>
```

**Site search:**
```js
function onSearch(query) {
  window.ca.track("site_search", { query: query.slice(0, 200) })
  // ... your search logic
}
```

Then ask your agent: "What are the most common search terms on my site?"

**Form submissions:**
```js
form.addEventListener("submit", () => {
  window.ca.track("form_submit", { form: "contact", page: location.pathname })
})
```

**Signup / login:**
```js
window.ca.track("signup_click", { method: "google" })
window.ca.track("signup_complete", { plan: "free" })
```

## Server-side tracking

For API products, background jobs, and webhook handlers where there's no browser, send events directly via HTTP:

```js
async function trackEvent(event, props) {
  try {
    await fetch("https://lodd.dev/v1/track", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Tracking-Secret": process.env.TRACKING_SECRET,
      },
      body: JSON.stringify({
        site_id: process.env.SITE_ID,
        type: "event",
        event_name: event,
        properties: props || {},
        session_id: crypto.randomUUID(),
        url: "https://your-api.com/internal",
        timestamp: new Date().toISOString(),
        browser: "server", os: "node", device_type: "server",
      }),
    });
  } catch { /* tracking is best-effort */ }
}
```

No SDK required. 15 lines. Fire and forget. If it fails, your app doesn't notice.

### Actor-based analytics

Pass an `actor` — a hashed identifier — to enable per-actor analytics:

```js
// Browser — use a server-rendered hash (Web Crypto is async)
// Your server renders: window.__ACTOR = "{{ sha256(userId) }}"
window.ca.track("login", { method: "google" }, window.__ACTOR);

// Server (fetch snippet)
{ ...payload, actor: sha256(userId) }

// Node SDK
lodd.track("api_call", { endpoint: "/v1/query" }, { actor: sha256(userId) });
```

The actor is an opaque string. Lodd never sees the original identifier. Once events have actors, three new tools become available:

- **get_active_actors** — distinct actors with event counts and revenue
- **get_actor_activity** — event timeline for one actor
- **get_actor_retention** — cohort retention by actor

**Important:** never send raw emails, user IDs, or API keys as the actor. Always hash first. Lodd cannot and will not bridge actors to CRM records or external databases — this is a GDPR design choice, not a missing feature. The actor is a one-way hash; Lodd has no way to reverse it.

**Note on retention accuracy:** actor identity is only as stable as the identifier you hash. For best results, hash a stable backend ID rather than a client-side token.

## MCP tools reference

42 tools total: 2 authentication + 35 authenticated.

Note: `get_timeseries` returns `{ buckets: [...], annotations?: [...] }` — annotations from the same period are included automatically. `get_snapshot` includes `last_annotation` when one exists from the past 7 days.

### Authentication (unauthenticated)
| Tool | Description |
|------|-------------|
| `authenticate(email)` | Send verification code to email |
| `verify_code(email, code)` | Exchange code for API key |

### Sites
| Tool | Description |
|------|-------------|
| `list_sites()` | All sites you own |
| `create_site(name, domain)` | Returns site ID, tracking secret, script tag |
| `exclude_my_ip(site)` | Stop tracking your own visits (per site) |

### Analytics
| Tool | Description |
|------|-------------|
| `get_snapshot(site)` | Today vs yesterday quick comparison |
| `get_analytics(site, period, filters?)` | Aggregate stats with period comparison |
| `get_timeseries(site, period, interval?, filters?)` | Hourly or daily time buckets |
| `get_funnel(site, period, steps, filters?)` | Multi-step conversion funnel (pageviews + events) |
| `get_realtime(site)` | Active visitors in last 5 minutes |
| `get_performance(site, period, group_by?)` | Page load time avg/median/p95 |

### Breakdowns
| Tool | Description |
|------|-------------|
| `get_pages(site, period, url_contains?, filters?)` | Top pages with bounce rate + avg duration |
| `get_traffic_sources(site, period, filters?)` | Referrers, UTM, trackable links with engagement |
| `get_countries(site, period, filters?)` | Visitors by country with bounce rate |
| `get_tech_breakdown(site, period, filters?)` | Browser, OS, device distribution |
| `get_entry_exit_pages(site, period, filters?)` | Where sessions start and end |
| `get_bot_report(site, period)` | Bot/crawler traffic by user agent |

### Conversion attribution
| Tool | Description |
|------|-------------|
| `get_conversion_pages(site, event_name, period)` | Pages viewed before a conversion, with rate + time |
| `get_source_conversions(site, event_name, period)` | Traffic sources ranked by conversion rate |

### Custom events
| Tool | Description |
|------|-------------|
| `get_event_counts(site, period, filters?)` | Event totals, sessions, revenue |
| `get_events(site, period, event_name?, limit?)` | Individual event records with properties |
| `get_event_timeseries(site, event_name, period, filters?)` | One event over time |

### Usage
| Tool | Description |
|------|-------------|
| `get_usage()` | Plan, events used, monthly limit |

### Key management
| Tool | Description |
|------|-------------|
| `create_api_key(name?)` | Generate a new API key (shown once) |
| `list_api_keys()` | All keys with status and last used |
| `revoke_api_key(key_id)` | Permanently deactivate a key |

### Actor analytics
| Tool | Description |
|------|-------------|
| `get_active_actors(site, period, limit?)` | Distinct actors with event counts, first/last seen, revenue |
| `get_actor_activity(site, actor, period, limit?)` | Event timeline for one actor hash |
| `get_actor_retention(site, period)` | Cohort retention by actor |

### Annotations
| Tool | Description |
|------|-------------|
| `create_annotation(site, content, timestamp?)` | Record a user-facing change (deploy, redesign, campaign) |
| `list_annotations(site, period)` | List annotations within a time period |

### Trackable links
| Tool | Description |
|------|-------------|
| `create_trackable_link(site, destination_url, source_type, label?)` | Short URL with source attribution |
| `list_trackable_links(site, status?)` | All links with click stats |
| `get_link_clicks(link, period)` | Click data for one link |

### Team access
| Tool | Description |
|------|-------------|
| `share_site(site, email)` | Give another user access to a site (owner only) |
| `list_members(site)` | List users with access and their roles |
| `remove_member(site, email)` | Remove a user's access (owner only) |

## Filters

Most analytics and breakdown tools accept optional filters:

| Filter | Format | Example |
|--------|--------|---------|
| `filter_country` | 2-letter ISO code | `"US"`, `"DE"`, `"NO"` |
| `filter_browser` | Substring match | `"Chrome"`, `"Safari"` |
| `filter_os` | Substring match | `"iOS"`, `"Windows"` |
| `filter_device_type` | Exact match | `"desktop"`, `"mobile"`, `"tablet"` |
| `filter_utm_source` | Exact match | `"twitter"`, `"newsletter"` |
| `filter_referrer_contains` | Substring match | `"google"`, `"reddit"` |

Breakdown tools exclude the dimension they group by (e.g. `get_countries` doesn't accept `filter_country`).

## Period formats

| Format | Meaning |
|--------|---------|
| `"today"` | Since midnight |
| `"yesterday"` | Previous full day |
| `"7d"` | Last 7 days |
| `"30d"` | Last 30 days |
| `"90d"` | Last 90 days |
| `"YYYY-MM-DD..YYYY-MM-DD"` | Custom range (up to 365 days) |

## Privacy

- No cookies
- No fingerprinting
- Country-only geolocation (no city, no coordinates)
- IPs are hashed with a daily-rotating salt, deleted after 24 hours
- GDPR compliant without consent banners
- Bot traffic is tagged and excluded from analytics

## How events are counted

Every page view and every custom event counts as one event toward your monthly limit. A visitor loading a page is one event. A `window.ca.track('signup_click')` call is one event. Server-side tracking via the Node SDK or HTTP API also counts one event per call. Bot traffic is detected and excluded automatically — bots do not count toward your limit.

## Pricing

Free up to 2,500 events/month (page views + custom events combined). €9.99/month for 100,000 events. All features on both tiers. See [/pricing.md](/pricing.md) for details.
