# Irv for Agents — Full Reference

> One fetch. Everything an AI agent needs to sign up, instrument an app, pull data, and operate against Irv's API.
> Last updated: 2026-06-10. Canonical URL: `https://irv.dev/agents`. Companion to /api/openapi.json (formal spec).

You are reading the long-form agent reference. If you only have room for one Irv doc, this is it.

**This doc lives at three URLs** — pick whichever your discovery flow uses:
- `https://irv.dev/agents` — canonical entry (the URL humans tell agents to visit)
- `https://irv.dev/llms-full.txt` — `llms.txt` convention alias
- `https://irv.dev/.well-known/llms.txt` — short summary that points back at this

**Quick navigation**:
1. Quick orientation
2. Sign up (3 paths to a PAT)
3. Verify your identity
4. Project setup + concepts
5. Integrate into an app (snippet, SDK, direct ingest)
6. Pull out data (freshness, live, sessions, analytics)
7. Read AI insights + ask questions
8. Quota + cost
9. Common agent recipes (copy-paste)
10. Error model + rate limits
11. CLI + MCP — alternative transports
12. Product overview + privacy + pricing
13. Discoverability summary
14. Changelog (last 30 entries)

**Audience**: AI agents (Claude, Codex, custom bots) and the humans writing prompts for them. Structure is flat + parseable. No graphics, no progressive disclosure, no JS.

---

# 1. Quick orientation

- **Base URL**: `https://irv.dev/api/v1`
- **Auth**: `Authorization: Bearer <PAT>` on every request. Exceptions: `POST /api/v1/ingest` (project-key auth, no PAT), `POST /api/v1/auth/device` (public, no auth), `POST /api/v1/agent-beta/provision` (beta-key auth, not a PAT)
- **PAT format**: 64-char hex (e.g. `0123abcd…ef`)
- **Beta-key format**: `bk_` + 32 hex chars (e.g. `bk_a1b2c3…`)
- **Project-key format**: `pk_` + 22 chars (e.g. `pk_xyz…`)
- **Scopes**: `read` < `write` < `admin`. Admin implies write implies read.
- **Errors**: `{ "error": "<code>", "message"?: "...", "hint"?: "..." }` with HTTP 4xx/5xx
- **Canonical agent URL**: `https://irv.dev/agents` — this doc. Tell your users to point you here if they want you to know how to use Irv.
- **Discovery URLs**:
  - This doc (canonical): `https://irv.dev/agents`
  - This doc (`llms.txt` alias): `https://irv.dev/llms-full.txt`
  - Short summary: `https://irv.dev/.well-known/llms.txt`
  - OpenAPI 3.1 spec: `https://irv.dev/api/openapi.json`
  - Autonomous quickstart (browser-readable): `https://irv.dev/docs/agents/quickstart`
  - Agent overview (human view): `https://irv.dev/docs/agents`
- **MCP server**: `https://irv.dev/api/mcp` (JSON-RPC 2.0, same PAT)

**Pick your starting point**:
- You have a beta key → §2A (autonomous provision)
- You have an email and a day to wait → §2B (hand-reviewed application)
- You have a human collaborator at a browser → §2C (device-code flow)
- You already have a PAT → skip to §3 (verify identity)

---

# 2. Sign up — get a PAT

Every call to `/api/v1/*` needs a Personal Access Token. Three paths to one.

## 2A. Autonomous: agent-beta provision

The agent provisions itself end-to-end. No browser, no Clerk sign-up, no human in the loop.

**Prereqs**: a beta key (`bk_…`) handed to you out-of-band by the Irv creator. The mint UI lives at `/app/admin/agent-beta` (creator-only). Ask in #ops or email matt@irv.dev to get one.

**Call**:

```
POST https://irv.dev/api/v1/agent-beta/provision
Authorization: Bearer <bk_xxx>
Content-Type: application/json

{
  "agent_name": "Codex CLI · Matt",     // optional but recommended; used as the PAT label
  "project_name": "Demo App",            // optional; defaults to "<agent_name> sandbox"
  "contact_email": "you@example.com"     // optional; recorded for revocation outreach
}
```

**Response (200)**:

```
{
  "pat": "0123abcd…ef",                  // 64-char hex — store NOW, only shown once
  "project_id": "proj_…",
  "project_key": "pk_…",                 // embed in client code for ingest
  "project_name": "Demo App",
  "agent_id": "agent_…",                 // your synthetic identity
  "agent_name": "Codex CLI · Matt" | null,
  "contact_email": "..." | null,
  "dashboard_url": "https://irv.dev/app/projects/proj_…",
  "snippet_url": "https://irv.dev/i.js?p=pk_…",
  "snippet_html": "<script async src=\"…\"></script>",
  "next_steps": {
    "verify_token": "https://irv.dev/api/v1/me",
    "ingest": "https://irv.dev/api/v1/ingest",
    "freshness": "https://irv.dev/api/v1/projects/proj_…/events-freshness",
    "docs": "https://irv.dev/docs/agents/quickstart"
  }
}
```

**Errors specific to provision**:

| Status | error              | meaning                                                        |
|--------|--------------------|----------------------------------------------------------------|
| 401    | missing_bearer     | No Authorization header. Add `Authorization: Bearer bk_…`    |
| 401    | wrong_token_kind   | You sent a PAT (`0123abcd…`) instead of a beta key (`bk_…`) |
| 401    | invalid_beta_key   | Key doesn't exist or was revoked                               |
| 429    | rate_limited       | This beta key is capped at 10 provisions/hour                  |

**After provision**: each call mints a fresh synthetic identity (`agent_<hash>`) and a fresh project. There is no "re-claim previous identity" path in v1 — store the PAT and reuse it.

## 2B. Hand-reviewed application

For real agent products where a human will own the account.

```
POST https://irv.dev/api/v1/agent-beta/apply
Content-Type: application/json

{
  "email": "you@example.com",                       // required — gets the invite
  "agent_name": "Acme Bot",                         // required
  "use_case": "Reads insights, posts in Slack.",    // required, ≤600 chars
  "scopes_requested": ["read", "write"],            // optional
  "expected_volume": "~50 req/day",                 // optional
  "built_with": "Claude Desktop",                   // optional
  "homepage": "https://example.com/agent"           // optional
}
```

The creator hand-reviews. On approval, the contact email gets an invite code → `/access?code=XXXX` → Clerk sign-up → on first `/app/account` visit a PAT is auto-minted with the requested scopes. Then the agent uses that PAT.

This path takes ~1 day. Use 2A if you can.

## 2C. Device-code flow (agent + human collaborator)

Same pattern as GitHub CLI / Vercel CLI / Anthropic CLI auth.

**Step 1**: agent requests a code.

```
POST https://irv.dev/api/v1/auth/device
Content-Type: application/json

{ "client_name": "My agent", "scopes": ["read","write"] }
```

→
```
{
  "user_code": "ABCD-EFGH",
  "device_code": "<opaque>",
  "verification_uri": "https://irv.dev/device",
  "verification_uri_complete": "https://irv.dev/device?code=ABCD-EFGH",
  "expires_in": 600,
  "interval": 5
}
```

**Step 2**: agent shows the human: "Visit `verification_uri` and enter `user_code`."

**Step 3**: agent polls every `interval` seconds:

```
POST https://irv.dev/api/v1/auth/device/poll
Content-Type: application/json

{ "device_code": "<opaque>" }
```

→ One of:
- `{ "error": "authorization_pending" }` — keep waiting
- `{ "error": "denied" }` — user denied; give up
- `{ "error": "expired" }` — code expired (600s); start over
- `{ "token": "0123abcd…", "scopes": [...], "expires_at": "..." }` — ✓ done

---

# 3. Verify your identity works

Always do this first — catches any auth issue before you waste calls.

```
GET https://irv.dev/api/v1/me
Authorization: Bearer <pat>
```

**Response (200)**:

```
{
  "user_id": "user_…" | "agent_…",
  "email": "..." | null,                  // null for synthetic agent identities
  "auth": {
    "via": "token" | "session",
    "scopes": ["read", "write"],
    "token_label": "Codex CLI · Matt"
  },
  "projects": [
    { "id": "proj_…", "key": "pk_…", "name": "My App", "created_at": "..." }
  ]
}
```

**Halt and surface an error if**:
- status ≠ 200 → your PAT is bad
- `auth.scopes` lacks the scope this run needs (some endpoints require `write` or `admin`)
- the project you expect isn't in `projects` → wrong PAT, or it was minted for a different identity

---

# 4. Project setup + concepts

## 4.1 Concepts

| Identifier      | Format                  | Where it's used                                                       |
|-----------------|-------------------------|-----------------------------------------------------------------------|
| project_id      | `proj_<16 chars>`     | Path params in `/api/v1/projects/{id}/*`                            |
| project_key     | `pk_<22 chars>`       | Query param for reads (`?project_key=pk_…`), ingest body, snippet src |
| read_token      | `irv_read_…`          | Public-page tokens for embedded live ribbons (most agents don't need) |

`project_id` is the stable internal handle. `project_key` is the publicly-embeddable key the snippet uses. Both come back from provisioning.

## 4.2 List your projects

Covered by `GET /api/v1/me`. If you just want the list:

```
GET https://irv.dev/api/projects
Authorization: Bearer <pat>
```

## 4.3 Create another project

```
POST https://irv.dev/api/projects
Authorization: Bearer <pat>
Content-Type: application/json

{
  "name": "Another App",                       // optional; defaults to a derived name
  "lovable_url": "https://x.lovable.app",      // optional; seeds allowed_origins
  "builder": "lovable"                         // optional; one of: lovable, v0, claude, bolt, cursor, replit, custom
}
```

→
```
{
  "project": { "id": "proj_…", "project_key": "pk_…", "name": "...", "created_at": "..." },
  "read_token": "irv_read_…"     // shown plaintext once — only needed for embedded live ribbons
}
```

---

# 5. Integrate Irv into an app

Three paths. Pick by what you're instrumenting.

## 5.1 Browser autocapture (recommended for web apps)

One line in the page's `<head>`:

```html
<script async src="https://irv.dev/i.js?p=<project_key>"></script>
```

After load you have `window.irv` and the SDK is auto-tracking pageviews, clicks, form submits, web vitals.

**Captured automatically** (no code):
- `$pageview` on every route change (incl. `history.pushState`)
- `$click` on any element with a `data-irv` attribute (or globally if `autocapture: true`)
- `$form_submit` on form submissions (field VALUES are never captured — only counts)
- `$web_vitals` (LCP, FID, CLS, INP, TTFB) — one event per page load
- Session metadata (`$session_id`, browser, OS, viewport)
- Outbound clicks, JS errors, 404s, scroll depth

**Programmatic API** exposed at `window.irv`:

| Method                                  | Purpose                                                       |
|-----------------------------------------|---------------------------------------------------------------|
| `irv.init(options)`                   | Re-init with options. Called once by the snippet auto.        |
| `irv.track(event, properties?)`       | Send a custom event                                           |
| `irv.identify(emailOrId, traits?)`    | Tie the current visitor to a user identity                    |
| `irv.alias(newId, existingId?)`       | Merge two identities                                          |
| `irv.group(type, key, traits?)`       | Bind the visitor to a B2B group (workspace, account, etc.)    |
| `irv.register(props)`                 | Sticky super-properties merged into every future event        |
| `irv.registerOnce(props)`             | Sticky props that only set on first call                      |
| `irv.unregister(key)`                 | Remove a sticky property                                      |
| `irv.optOut()` / `irv.optIn()`      | DNT-style consent toggle (persists in localStorage)           |
| `irv.hasOptedOut()`                   | Boolean — is the visitor currently opted out                  |
| `irv.flush()`                         | Manually flush the queue                                      |

**`irv.init` options**:

```js
irv.init({
  projectKey: "pk_…",            // required if not in src=?p=
  apiHost: "https://irv.dev",    // override for self-hosted
  autocapture: true,             // default true
  debug: false,                  // verbose console logging
  respectDnt: true,              // honour navigator.doNotTrack + Sec-GPC (default false)
  filterBots: true,              // drop headless / Lighthouse / crawler UAs (default true)
  persistQueue: true,            // localStorage queue for crash recovery (default true)
  webVitals: true,
  superProperties: { app_version: "1.4.2" },
  beforeSend: (event) => event,  // hook to mutate or drop events; return null to drop
});
```

## 5.2 NPM packages

`@irv/web` (TypeScript) wraps the snippet for module-based apps:

```
npm install @irv/web
```

```ts
import { irv } from "@irv/web";
irv.init({ projectKey: process.env.NEXT_PUBLIC_IRV_PROJECT_KEY! });
irv.track("checkout_started", { plan: "pro", cents: 4900 });
irv.identify("user@example.com", { plan: "pro" });
```

`@irv/next` is a thin Next.js wrapper that injects the script in App Router.

`@irv/server` is a Node 18+ server-side client (same surface, server-targeting):

```ts
import { irv } from "@irv/server";
const client = irv({ projectKey: process.env.IRV_PROJECT_KEY });
client.track("agent_completed", { tools_called: 12 }, { distinctId: "user@example.com" });
```

## 5.3 Direct ingest (server-side, any language)

For background workers, mobile backends, CLI tools — anything that can POST.

```
POST https://irv.dev/api/v1/ingest
Content-Type: application/json

{
  "events": [
    {
      "project_key": "pk_…",        // required, shared by every event in the batch
      "event": "checkout_started",  // required, snake_case noun_verb is the convention
      "timestamp": 1717945938000,   // optional; epoch ms; server stamps if omitted
      "distinct_id": "user_42",     // optional; the identity the event is for
      "session_id": "sess_abc",     // optional; ties events into a session
      "event_id": "evt_uniq_x",     // optional; for at-least-once dedup
      "properties": {                // optional, free-form
        "plan": "pro",
        "cents": 4900
      }
    }
  ]
}
```

**Rules**:
- All events in one batch must share `project_key`.
- `project_key` must be known (`pk_demo` is the only public exception).
- If `allowed_origins` is set on the project and the request has an `Origin` header that doesn't match → 403.
- No hard batch-size limit at v1, but keep payloads <1 MB for predictable latency.

**Auth**: NO PAT. The endpoint is project-key gated only — that's why the browser snippet works from a public webpage.

## 5.4 Event naming conventions

- **Custom events**: `snake_case_noun_verb` — `checkout_started`, `signup_completed`, `pricing_viewed`
- **Reserved** (don't override): `$pageview`, `$click`, `$form_submit`, `$identify`, `$alias`, `$group`, `$web_vitals`, `$set`, `$session_start`
- **Reserved property names**: `$session_id`, `$distinct_id`, `$timestamp`, `$browser`, `$os`, `$url`, `$referrer`, `$device_type`, `$utm_source`, `$utm_medium`, `$utm_campaign`

---

# 6. Pull out data

## 6.1 Freshness check (one number, cheapest)

The best polling endpoint while you're verifying instrumentation.

```
GET https://irv.dev/api/v1/projects/<project_id>/events-freshness?since=5m
Authorization: Bearer <pat>
```

`since` accepts `5m` / `30s` / `2h` / raw milliseconds. Clamped to 24h.

→
```
{
  "project_id": "proj_…",
  "project_key": "pk_…",
  "since_ms": 300000,
  "count": 12,
  "last_event_at": "2026-06-09T11:42:18.231Z",
  "last_event_name": "$pageview"
}
```

`count > 0` means data is landing.

## 6.2 Recent events

```
GET https://irv.dev/api/v1/live?project_key=<pk>&limit=30
Authorization: Bearer <pat>
```

→
```
{
  "project_key": "pk_…",
  "totals": { "all_time": 12030, "last_24h": 420 },
  "recent": [{ "event": "$pageview", "timestamp": 1717…, "distinct_id": "...", "session_id": "...", "properties": {...} }, ...],
  "daily_counts": [{ "date": "2026-06-03", "count": 240 }, ...]
}
```

## 6.3 Sessions

```
GET https://irv.dev/api/v1/sessions?project_key=<pk>&interesting=true&limit=20
```

→ `{ "sessions": [{ "session_id": "...", "distinct_id": "...", "started_at": "...", "ended_at": "...", "event_count": 8, "duration_ms": 245000, ... }] }`

`interesting=true` filters to multi-event sessions (single-bounce `$pageview` sessions excluded).

```
GET https://irv.dev/api/v1/sessions/<session_id>?project_key=<pk>
```

→ The full event stream of one session.

## 6.4 Project rollup analytics

```
GET https://irv.dev/api/v1/analytics?project_key=<pk>&range=7d
```

`range` accepts `24h` / `7d` / `30d` / `90d`. Default `7d`.

→
```
{
  "project_key": "pk_…",
  "range": "7d",
  "as_of": "2026-06-09T11:00:00Z",
  "totals": {
    "visitors": 1820,
    "pageviews": 5403,
    "sessions": 2104,
    "bounce_rate": 0.42
  },
  "top_pages": [{ "path": "/", "visitors": 980, "pageviews": 1402 }, ...],
  "sources": [{ "source": "direct", "visitors": 720 }, ...],
  "deltas_vs_previous": { "visitors": { "abs": 120, "pct": 7.1 }, ... },
  "daily": [{ "date": "2026-06-03", "visitors": 240, "pageviews": 712 }, ...]
}
```

## 6.5 Group analytics (B2B)

```
GET https://irv.dev/api/v1/analytics/groups?project_key=<pk>&group_type=workspace
```

→ `{ "groups": [{ "group_key": "...", "group_type": "workspace", "visitors": 12, "sessions": 24, ... }] }`

Only meaningful if you've called `irv.group(...)` from the client or sent `$group_key` in event properties.

## 6.6 Tracking plan + debt

```
GET https://irv.dev/api/v1/tracking-plan?project_key=<pk>
```
→ Cached, regex-derived "what events does this project send" schema.

```
GET https://irv.dev/api/v1/tracking-debt?project_key=<pk>
```
→ Events seen in the last 7 days that aren't in the tracking plan — useful for "what new instrumentation showed up."

---

# 7. Read AI insights + ask questions

## 7.1 Cached insights

```
GET https://irv.dev/api/v1/insights?project_key=<pk>
```

→
```
{
  "insights": [
    {
      "id": "...",
      "card": { "title": "…", "body": "…" },
      "category": "drop_off",                       // drop_off | page_improvement | tracking_gap | source_attribution | change_attribution | anomaly
      "severity": "warn",                           // info | warn | critical
      "confidence": 78,                             // 0–100
      "evidence": { "metric_refs": [...], "page_paths": [...], "commit_sha": "...", "file_paths": [...] },
      "top_causes": [{ "description": "…", "evidence_kind": "deploy", "confidence": 82 }, ...],
      "lovable_prompt": "…"                          // optional — a ready-to-paste fix prompt
    }
  ]
}
```

## 7.2 Refresh insights (write scope)

```
POST https://irv.dev/api/v1/insights/refresh?project_key=<pk>
Authorization: Bearer <pat-with-write-scope>
```

Re-runs the LLM pass over the project's data. ~$0.05 per refresh. Rate-limited per-project, per-day budget.

## 7.3 Dismiss / track an insight

```
POST https://irv.dev/api/v1/insights/<insight_id>/dismiss?project_key=<pk>
GET  https://irv.dev/api/v1/insights/<insight_id>/track?project_key=<pk>
```

## 7.4 Daily brief

```
GET  https://irv.dev/api/v1/daily-brief?project_key=<pk>            // cached, 6h TTL
POST https://irv.dev/api/v1/daily-brief?project_key=<pk>            // regen (~$0.05)
GET  https://irv.dev/api/v1/daily-brief/stream?project_key=<pk>     // SSE stream of regen
```

Response includes `headline`, `executive_summary`, `metric_movements[]` with cause + confidence, `priorities[]` (action / expected_impact / why_now), `watchpoints[]`, `continuity_from_yesterday`.

## 7.5 Weekly narrative

```
GET https://irv.dev/api/v1/weekly-narrative?project_key=<pk>
```

Six-paragraph executive narrative (state of business / what worked / what didn't / where to look / recommendations / metric to watch).

## 7.6 Outcomes — what did past shipped insights actually move?

```
GET https://irv.dev/api/v1/insights/outcomes?project_key=<pk>&limit=10
```

→
```
{
  "outcomes": [
    {
      "insight_id": "…",
      "insight_title": "…",
      "shipped_at": "2026-05-22T…",
      "shipped_platform": "lovable",
      "evaluations": [
        { "at_day": 7, "was_improvement": true, "delta_pct_visitors": 12.4, "before": 800, "after": 899, "evaluated_at": "..." },
        { "at_day": 14, "was_improvement": true, "delta_pct_visitors": 18.1, ... }
      ]
    }
  ]
}
```

## 7.7 Causation — patterns + counterfactuals + anomalies

```
GET  https://irv.dev/api/v1/causation?project_key=<pk>
POST https://irv.dev/api/v1/causation?project_key=<pk>           // refresh
```

→ `{ "correlations": { "top": [...], "notes": [...], "generated_at": "...", "range_days": 14 }, "counterfactuals": [...], "anomalies": { "anomalies": [...], "generated_at": "..." } }`

All findings have passed significance gates (Pearson + Spearman with FDR correction, Granger F-test for direction, STL+3σ for anomalies). Irv only surfaces patterns the math already confirmed.

## 7.8 Ask Irv (natural language queries)

```
POST https://irv.dev/api/v1/ask?project_key=<pk>
Authorization: Bearer <pat-with-write-scope>
Content-Type: application/json

{ "question": "Why did /pricing bounce go up this week?" }
```

→
```
{
  "answer": "Bounce on /pricing rose from 38% to 51% Tue–Thu, driven by …",
  "citations": [
    { "type": "metric", "name": "bounce_rate", "value": 0.51, "ref": "/pricing" },
    { "type": "deploy", "sha": "a1b2c3d", "at": "2026-06-04T14:22:00Z" }
  ],
  "trace": [
    { "tool": "get_analytics", "args": { "page": "/pricing", "range": "7d" }, "result_summary": "..." },
    ...
  ]
}
```

Streaming variant (token-by-token, tool-by-tool):

```
GET https://irv.dev/api/v1/ask?project_key=<pk>&question=<url-encoded>
Accept: text/event-stream
```

SSE events: `{ type: "token", text: "..." }`, `{ type: "tool_call", name: "...", args: {...} }`, `{ type: "done", answer: "...", citations: [...] }`.

## 7.9 Subscribe to events (webhooks) — react instead of poll

Register a URL; Irv POSTs to it when something worth acting on appears. This is how a monitoring agent goes from "poll insights every hour" to "get woken up the moment a critical insight lands."

**Create** (PAT with write scope; you must own the project):

```
POST https://irv.dev/api/v1/subscriptions
Authorization: Bearer <pat>
Content-Type: application/json

{
  "project_key": "pk_…",
  "url": "https://your-agent.example.com/hooks/irv",
  "events": ["insight.critical", "anomaly.detected"]
}
```

→ `{ "id": "sub_…", "secret": "whsec_…", ... }` — **the secret is shown once.** Store it; you verify deliveries with it.

**Event vocabulary**:

| Event              | Fires when                                                      |
|--------------------|------------------------------------------------------------------|
| `insight.created`  | Any genuinely-new insight appears after a refresh               |
| `insight.critical` | A new insight with `severity: "critical"` appears             |
| `anomaly.detected` | A new STL+3σ anomaly is found by the causation pass             |

**Delivery format** — POST to your URL with headers:

```
X-Irv-Event:     insight.critical
X-Irv-Delivery:  <uuid, unique per attempt>
X-Irv-Signature: sha256=<hmac-sha256 hex of the raw body, keyed by your whsec_… secret>
```

Body:

```
{
  "event": "insight.critical",
  "project_key": "pk_…",
  "occurred_at": "2026-06-10T09:14:02.113Z",
  "data": {
    "id": "…",                       // CAUTION: regenerates on refresh — don't store long-term
    "fingerprint": "a1b2c3d4e5f60718", // stable content hash — idempotency-key on THIS
    "category": "drop_off",
    "severity": "critical",
    "card": { "title": "…", "body": "…" },
    "lovable_prompt": "…"            // the paste-ready fix
  }
}
```

**Verify the signature** (Node):

```js
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
```

**Semantics + hygiene**:
- At-most-once with one immediate retry. A missed webhook self-heals on your next poll — every payload is re-derivable from `GET /api/v1/insights`.
- Respond `2xx` within 5 seconds. Do real work async — `202` immediately, process after.
- Dedup on `data.fingerprint`, not `data.id` (ids regenerate every refresh).
- 10 consecutive failures auto-disables the subscription. Recovery: `DELETE` it + recreate (fresh secret).
- HTTPS required (plain http allowed for localhost during development).

**List + delete**:

```
GET    https://irv.dev/api/v1/subscriptions          → your subscriptions, secrets redacted, delivery health included
DELETE https://irv.dev/api/v1/subscriptions/{id}     → remove (also the re-enable path for auto-disabled subs)
```

---

# 8. Quota + cost

```
GET https://irv.dev/api/v1/ai-quota?surface=ask_question
```

→ `{ "surface": "ask_question", "used": 12, "limit": 50, "resets_at": "2026-06-10T00:00:00Z" }`

Surfaces with quotas: `ask_question`, `daily_brief`, `insights_refresh`, `weekly_narrative`.

```
POST https://irv.dev/api/v1/ai-quota/request-more
Content-Type: application/json
Authorization: Bearer <pat>

{ "surface": "ask_question", "justification": "..." }
```

Creates a request the creator sees in admin. Approved requests bump the cap.

**BYOK bypass**: if the user has set their own Anthropic key at `/app/account` (or via `POST /api/v1/account/byok`), AI surfaces run on their bill and bypass platform caps.

## 8.1 Canvases (analytical workspaces)

Free-form 2D boards where you can drop chart widgets + connect them with AI-interpreted edges.

```
GET    /api/v1/projects/<id>/canvases
POST   /api/v1/projects/<id>/canvases             { name, template_id? }
GET    /api/v1/projects/<id>/canvases/<canvas_id>
PATCH  /api/v1/projects/<id>/canvases/<canvas_id> { name?, widgets?, connections? }
GET    /api/v1/projects/<id>/canvases/<canvas_id>/analyze/stream   (SSE)
```

Most agents won't need canvases — they're a UI surface for human investigation. Mentioned for completeness.

---

# 9. Common agent recipes (copy-paste)

## 9A. Onboard a fresh project end-to-end (autonomous)

```bash
# 1. Provision
P=$(curl -sS -X POST "https://irv.dev/api/v1/agent-beta/provision" \
  -H "Authorization: Bearer $IRV_BETA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_name":"Demo","project_name":"Demo App"}')
PAT=$(echo "$P" | jq -r .pat)
PROJECT_ID=$(echo "$P" | jq -r .project_id)
PROJECT_KEY=$(echo "$P" | jq -r .project_key)

# 2. Verify
curl -sS "https://irv.dev/api/v1/me" -H "Authorization: Bearer $PAT" | jq .auth

# 3. Insert snippet into the user's HTML <head>:
#    <script async src="https://irv.dev/i.js?p=$PROJECT_KEY"></script>
#    (Done with your file-edit tools — see /docs/agents/quickstart for prompt templates.)

# 4. Fire a test event (skipping the dev server)
curl -sS -X POST "https://irv.dev/api/v1/ingest" \
  -H "Content-Type: application/json" \
  -d "{\"events\":[{\"project_key\":\"$PROJECT_KEY\",\"event\":\"agent_smoke\",\"timestamp\":$(date +%s%3N),\"distinct_id\":\"agent\",\"session_id\":\"s1\"}]}"

# 5. Verify data landed
for i in 1 2 3 4 5; do
  COUNT=$(curl -sS "https://irv.dev/api/v1/projects/$PROJECT_ID/events-freshness?since=2m" \
    -H "Authorization: Bearer $PAT" | jq -r .count)
  [ "$COUNT" -ge 1 ] && echo "✓ $COUNT events landed" && break
  sleep 2
done

# 6. Report
echo "$P" | jq -r '"Dashboard: " + .dashboard_url'
```

## 9B. Pull yesterday's headline + propose actions

```bash
curl -sS "https://irv.dev/api/v1/daily-brief?project_key=$PROJECT_KEY" \
  -H "Authorization: Bearer $PAT" | jq '{headline, executive_summary, priorities}'
```

If you need fresher analysis, POST first to regenerate. If you need on-demand analysis of a specific question, use `POST /api/v1/ask` instead — cheaper + more focused.

## 9C. Continuous monitoring loop

A daemon-style agent watching one project:

1. `/api/v1/me` once at startup → cache identity
2. `POST /api/v1/subscriptions` once → get woken on `insight.critical` + `anomaly.detected` instead of polling (see §7.9)
3. `/api/v1/projects/{id}/events-freshness?since=5m` every minute → freshness pulse
4. `/api/v1/insights?project_key=<pk>` every few hours → backstop poll in case a webhook was missed
5. `/api/v1/daily-brief?project_key=<pk>` once per morning UTC → post to user's chat
6. `/api/v1/ask?…` for ad-hoc questions

## 9D. Smoke-test your integration (10 lines)

```bash
# Assumes you've provisioned and have $PAT, $PROJECT_KEY, $PROJECT_ID in env.
set -e
curl -fsS "https://irv.dev/api/v1/me" -H "Authorization: Bearer $PAT" >/dev/null
curl -fsS -X POST "https://irv.dev/api/v1/ingest" \
  -H "Content-Type: application/json" \
  -d "{\"events\":[{\"project_key\":\"$PROJECT_KEY\",\"event\":\"smoke_$$\",\"timestamp\":$(date +%s%3N),\"distinct_id\":\"smoke\",\"session_id\":\"smoke\"}]}"
sleep 3
curl -fsS "https://irv.dev/api/v1/projects/$PROJECT_ID/events-freshness?since=1m" \
  -H "Authorization: Bearer $PAT" | jq -e '.count >= 1' >/dev/null && echo "✓ integration healthy"
```

## 9E. Close the loop — self-healing app

The full circle: Irv finds the problem, wakes you, you ship the fix, Irv measures whether it worked. This is the recipe the platform is built around.

```
1. SUBSCRIBE (once, at setup):
   POST /api/v1/subscriptions
     { project_key, url: <your endpoint>, events: ["insight.critical"] }
   → store the whsec_… secret.

2. WAKE: Irv POSTs to your endpoint when a critical insight lands.
   Verify X-Irv-Signature. The payload carries the insight including
   lovable_prompt (the paste-ready fix) + a stable fingerprint.

3. FETCH CONTEXT (optional but smart):
   GET /api/v1/insights?project_key=…       → full current insight set
   GET /api/v1/analytics?project_key=…      → the numbers behind it
   The webhook payload is a summons, not the whole case file.

4. SHIP THE FIX: apply lovable_prompt (or your own judgment) to the
   codebase with your file-edit tools. Open a PR or commit directly —
   your call, your repo conventions.

5. MARK SHIPPED — starts the outcome clock:
   POST /api/v1/insights/{insight_id}/track?project_key=…&event=shipped&platform=claude-code
   (or the MCP tool: mark_shipped { project_key, insight_id, platform })
   NOTE: insight ids regenerate on refresh. Use the id from the webhook
   payload promptly, or re-list insights and match on fingerprint/title.

6. MEASURE: Irv snapshots the cited metric at ship time and evaluates
   at day 7 + day 14 (DiD, bootstrap CI).
   GET /api/v1/insights/outcomes?project_key=…
   → was_improvement: true | false | null, delta_pct_visitors, …

7. REPORT: tell your human what you shipped and, a week later, what
   it actually moved. That second message is the one they remember.
```

The webhook step is optional — the loop also works fully pull-based (poll insights, act on criticals). Webhooks just collapse the latency from "next poll" to "right now".

---

# 10. Error model + rate limits

Every error responds with:

```
{
  "error": "<machine_readable_code>",
  "message"?: "<human prose>",
  "hint"?: "<suggestion>"
}
```

Common codes:

| Status | error                       | meaning                                                  |
|--------|-----------------------------|----------------------------------------------------------|
| 400    | invalid_json                | Body wasn't valid JSON                                   |
| 400    | invalid_payload             | Body parsed but shape was wrong                          |
| 400    | missing_<field>             | Required field absent                                    |
| 401    | unauthorized                | No or bad auth                                           |
| 401    | missing_bearer              | Auth header absent                                       |
| 401    | wrong_token_kind            | Sent the wrong artifact (e.g. PAT to a beta-key endpoint) |
| 401    | invalid_beta_key            | Beta key unknown or revoked                              |
| 403    | forbidden                   | Auth valid but lacks required scope or ownership         |
| 403    | scope_required              | Token doesn't have the scope this endpoint needs         |
| 403    | phone_verification_required | When the gate is enabled — verify a phone first          |
| 404    | entry_not_found             | Resource doesn't exist                                   |
| 404    | unknown_project             | `project_key` not recognised                           |
| 409    | conflict                    | Idempotency violation                                    |
| 429    | rate_limited                | Cap hit; includes `retry_after_seconds`                |
| 500    | internal                    | Server error                                             |
| 502    | upstream_error              | LLM / DB / external dep failed                           |

**Common rate limits**:

| Endpoint                                   | Limit                                                          |
|--------------------------------------------|----------------------------------------------------------------|
| `POST /api/v1/agent-beta/provision`      | 10 per beta key per hour                                       |
| `POST /api/v1/agent-beta/apply`          | 5 per IP per hour                                              |
| `POST /api/v1/ask`                       | per-token; surfaced via `/api/v1/ai-quota?surface=ask_question` |
| `POST /api/v1/insights/refresh`          | per-project, per-day budget                                    |
| `POST /api/v1/feedback`                  | 10 per hour per user                                           |
| Per token (overall)                        | 1000 req/min                                                   |

**Recommended backoff**: exponential with jitter. On 429, honour `retry_after_seconds`. On 5xx, retry max 3 times.

---

# 11. CLI + MCP — alternative transports

Same auth, same surface, different ergonomics.

## 11.1 `irv-cli` (npm)

```bash
npm install -g irv-cli
irv login --token <pat>
irv use pk_…
irv stats --range=30d
```

Read-only by design. 14 commands. Same PAT it uses works against the HTTP and MCP transports too.

| Command                              | What it does                                             |
|--------------------------------------|----------------------------------------------------------|
| `irv login [--token <t>]`          | Save a PAT to the local config                           |
| `irv logout`                       | Clear the saved token                                    |
| `irv whoami`                       | Show API URL + token status                              |
| `irv projects`                     | List your projects                                       |
| `irv use <key>`                    | Set the default project                                  |
| `irv stats [--range=24h|7d|30d|90d]`| Headline metrics + breakdowns                            |
| `irv insights`                     | List active insights                                     |
| `irv outcomes`                     | Measured outcomes from shipped insights                  |
| `irv plan`                         | Code-derived tracking plan                               |
| `irv log [--since=24h]`            | Project event log                                        |
| `irv causation`                    | Correlations + counterfactuals + anomalies               |
| `irv install <key>`                | Print the SDK install snippet                            |
| `irv tools`                        | List MCP tools the server exposes                        |
| `irv ping`                         | Verify connectivity + auth                               |

## 11.2 MCP server (Claude Desktop, any MCP client)

Add Irv to claude_desktop_config.json:

```json
{
  "mcpServers": {
    "irv": {
      "url": "https://irv.dev/api/mcp",
      "headers": { "Authorization": "Bearer <pat>" }
    }
  }
}
```

The endpoint speaks JSON-RPC 2.0. Usable from any MCP client, not just Claude Desktop.

| MCP tool              | Returns                                                          |
|-----------------------|------------------------------------------------------------------|
| get_stats             | Headline metrics + WoW deltas + top dimensions                   |
| get_top_dimension     | Top pages / sources / countries / etc.                           |
| get_insights          | Active AI insights with confidence + ranked causes               |
| get_outcomes          | DiD lift + bootstrap CI for shipped insights                     |
| get_change_effects    | Commits + shipped prompts paired with metric movement            |
| get_engagement        | Outbound clicks / forms / errors / 404s / scroll                 |
| get_audience          | Channels + new-vs-returning + hourly distribution                |
| get_project_log       | Filterable timeline of every project event                       |
| get_tracking_plan     | Code-derived plan + tracking-debt grade                          |
| get_causation         | Correlations + Granger + DiD + anomalies                         |
| mark_shipped          | WRITE — mark an insight's fix shipped; starts the 7/14-day outcome clock (needs write scope) |

---

# 12. Product context (what Irv is)

Three things rolled together:

1. **Standard analytics** on a HyperLogLog backbone. Sub-50ms year-of-history queries. Visitors / pageviews / sessions / bounce / duration / countries / devices / browsers / OS / languages / UTM / channels / new-vs-returning / hourly distribution / top-clicked elements per page / forms / outbound clicks / JS errors / 404s / scroll depth / Web Vitals.

2. **AI surfaces.** Morning brief, weekly narrative, Ask Irv, auto-cohort discovery, canvas summaries — written by Claude Sonnet, grounded in real statistical primitives (Pearson + FDR + Granger + DiD) so the model only narrates findings that have passed significance.

3. **Code-aware everything.** Connect GitHub and Irv reads your repo, derives the tracking plan from source, comments on your PRs with predicted impact + tracking suggestions, opens companion auto-instrument PRs, pairs every commit with the metric movement around it, grades your instrumentation completeness.

Built for developers using AI builders (Lovable, v0, Bolt, Cursor, Replit, Claude Code) who want analytics without writing a tracking plan.

## 12.1 Privacy + security

- **No IPs stored.** Country derived from edge geo header.
- **No form values captured.** Autocapture counts fields, never reads them.
- **30-day raw event retention.** Aggregated rollups (HLL + counters) longer.
- **PII redaction on ingest.** Emails, phones, SSNs, IPv4 auto-stripped from custom event property values.
- **DNT + Sec-GPC respected** when `irv.init({ respectDnt: true })`.
- **Bot filtering** on by default.
- **Split tokens**: `pk_…` ingest keys can't read your data; reads require a separate `irv_read_` token or a Clerk session or a PAT.
- **Per-project Origin allowlist** for ingest (defense in depth).
- **Per-token rate limits** (1000 req/min per token).
- **AES-GCM encrypted at rest** for BYOK API keys.
- **GDPR endpoints**: `/api/v1/account/export` (Article 20), `/api/v1/account/delete` (Article 17). One-click in `/app/account`.
- **Audit log**: every authenticated read recorded to project_log.

Full notice: https://irv.dev/privacy. Terms: https://irv.dev/terms.

## 12.2 Pricing

Free during beta. No credit card. No usage caps that matter.

When paid tiers ship:
- **Free** — 1 project, limited AI surfaces, generous enough to USE
- **Pro** — ~$20/project/month, unlimited AI, 30-day retention, BYOK option
- **BYOK** — bring your own Anthropic key, free to you (you pay AI direct)

Beta users get Pro free for the first 6 months. We email before any change lands.

https://irv.dev/pricing

---

# 13. Discoverability summary

URLs an agent should remember (in priority order for cold-starting a new session):

1. `https://irv.dev/agents` — this doc, canonical entry. Tell humans to point you here.
2. `https://irv.dev/llms-full.txt` — same content, `llms.txt` convention alias.
3. `https://irv.dev/.well-known/llms.txt` — short summary that points back at #1.
4. `https://irv.dev/api/openapi.json` — OpenAPI 3.1 spec (machine-strict).
5. `https://irv.dev/docs/agents/quickstart` — autonomous onboarding recipe (human + agent friendly).
6. `https://irv.dev/docs/agents` — agent API overview with code samples (human friendly).

**Versioning**: every public endpoint is under `/api/v1`. Breaking changes get a new major version (`/api/v2`). Additive changes (new fields, new endpoints) ship under v1. This doc's "Last updated" header changes when the surface does.

**Stability of these URLs**: `/agents`, `/llms-full.txt`, and `/.well-known/llms.txt` won't move. Bookmark whichever fits your discovery flow. The other URLs in section 1 also won't move.

End of reference. If something here is wrong, file an issue at `https://github.com/irvdotdev/irv/issues` or email matt@irv.dev.

---

# 14. Changelog (most recent first)

## 2026-06-18 · The open-sign-up switch — one env var opens the doors
Category: feature

Built the actual switch the abuse defences were waiting for. `IRV_OPEN_SIGNUP=1` (`lib/signup-mode.ts`) turns `/waitlist` — the single chokepoint every 'Join the beta' CTA points to — into a redirect to the new public **`/sign-up`** page, which renders a Clerk `SignUpButton` straight into onboarding. No invite code. While the flag is unset (the default), `/sign-up` bounces back to the waitlist, so there's still no public sign-up until you flip it. Read per-request (`force-dynamic`), so the flip needs no code change — set `IRV_OPEN_SIGNUP=1` and `IRV_REQUIRE_PHONE_VERIFY=1` together (the human gate) and the doors are open, with the per-user $2 AI cap + per-IP throttle holding the line. Existing `/access` invite codes keep working.

## 2026-06-18 · Homepage now points Shopify stores to the Shopify version
Category: polish

Added a single compact callout to the homepage — 'Running a Shopify store? Irv reads your whole funnel from one Custom Pixel, no app' — linking to `/shopify`. Deliberately *not* a 'templates' section: only Web (default) and Shopify are real today, so the page names the one live vertical and nothing that isn't built yet. Same minimal.so idiom as the rest of the page (hairline divider, big type, a text-link with an arrow), slotted between 'How it works' and the close.

## 2026-06-18 · Open sign-up defences — phone gate + per-IP throttle on new projects
Category: security

Two server-side guards that make it safe to drop the waitlist and let anyone sign up, layered on the per-user $2/month AI cap. (1) The verified-phone gate (`IRV_REQUIRE_PHONE_VERIFY`) now covers project creation, not just token-minting — so every account costs an attacker a unique phone number; onboarding catches the 403 and routes to phone verification. (2) A per-IP throttle caps new projects per IP per hour (`IRV_PROJECTS_PER_IP_HOUR`, default 10) as defence-in-depth against a single-IP farm. Both are inert during the beta (phone gate permissive while the flag is unset; 10/IP/hour never bites a real user). The one consequential step — flipping public sign-up on — is deliberately left for a human; the checklist is in `docs/features/open-signup-defenses.md`.

## 2026-06-18 · Shopify projects no longer origin-locked — the pixel's events actually land
Category: infra

Fixed a silent killer for real Shopify stores: onboarding seeded `allowed_origins` from the store URL you type, but Shopify's Custom Pixel runs in a **sandboxed origin** (not the storefront domain), so ingest would have rejected every event with `origin_not_allowed` — an empty dashboard with no error. Shopify-type projects are now created with an open origin allowlist (the `pk_live` key is public + write-only, the same trust model as the web snippet), so a merchant's pixel works the moment it's pasted. The onboarding note no longer claims a non-existent origin lock. Web projects are unchanged — they still auto-lock to their domain.

## 2026-06-13 · A landing page for Shopify stores — /shopify
Category: polish

A dedicated marketing page at `/shopify` for store owners, in the same minimal.so design as the homepage but with store-specific, non-developer copy: paste one Custom Pixel (no app), get a plain-English morning report — what sold, where you're losing sales, the one thing to fix today. Three-step setup, six store-focused features (sales each morning, where shoppers drop, best/worst sellers, ask anything, breakage alerts, what-changed), and a Shopify-flavoured sample brief. Pairs with the Shopify template (the pixel + mapping + test-kit).

## 2026-06-17 · Pick Shopify at signup — onboarding installs the Custom Pixel for you
Category: feature

The Shopify template is now a one-click choice in the setup wizard. A new merchant signs in, picks **Shopify store** on the first screen (next to the Lovable/v0/Cursor builders), pastes their store URL, and gets the Custom Pixel — their real project key already baked in — with the exact Settings → Customer events steps. No JS snippet, no GitHub step (a store has no repo to patch); the live-event verifier still flips green the moment the first storefront event lands. Projects now carry an additive `type` field (`"shopify"`, default `"web"`) so the dashboard knows which taxonomy it's reading — the core ingest/analytics engine is untouched. The pasteable pixel is generated by `lib/shopify-pixel-source.ts`, whose event mapping is verified byte-for-byte against the unit-tested `lib/shopify-pixel.ts`.

## 2026-06-13 · Shopify — any store works with Irv via one Custom Pixel (first customer-type template)
Category: feature

A Shopify merchant pastes one Custom Pixel (Settings → Customer events, no app) and Irv sees the full storefront → cart → checkout → **purchase** funnel, with revenue, in the dashboards Irv already has. It's the first 'customer-type template' from the e-commerce plan — and it's purely additive: the core ingest/analytics/insight engine is unchanged. The pixel subscribes to Shopify's standard events (incl. `checkout_completed`, which a theme script can't reach) and POSTs mapped events to the existing `/api/v1/ingest` (already CORS-enabled). Money is normalized to `value_cents` + `currency` like the Stripe connector. The mapping lives once in `lib/shopify-pixel.ts` (unit-tested), mirrored by the pasteable pixel and a self-contained `tools/shopify-test-kit/` that replays a realistic Shopify session through the mapping into a real project and verifies it lands. Revenue aggregation (AOV/conversion/refund), the server-side Orders-webhook connector, and the e-commerce insight lens are documented next phases, deliberately not bolted into core.

## 2026-06-13 · Homepage redesign — minimal.so design, sharp non-dev copy
Category: polish

New homepage (v4). **Design**, in the minimal.so idiom: a left-aligned hero with a huge, tight headline; pill buttons (black primary + a plain text-link secondary with an arrow); monochrome chrome, hairline dividers, lots of whitespace. **Content**, cut hard and rewritten for non-developers — the page is a headline, ONE real morning-brief shot (the thing Irv actually makes), a brief feature grid (morning brief, Ask Irv, Autoheal, tracking gaps, **Canvas**, change tracking), a short plain-English 'how it works' (watches → analyses with significance tests + correlations → explains), and a close. ~75% shorter than the old homepage; the demo carousel, surface tour, and developer sections (SDK / MCP / CLI / agents) are gone. v3 is kept for one-line revert (swap the import in `app/page.tsx`).

## 2026-06-13 · Mobile web — the dashboard, phone-friendly and phone-detected
Category: feature

Opening Irv on a phone now gives a real mobile experience instead of a shrunk-to-fit desktop page. A proper `viewport` meta tag makes the app render at device width (readable text, no horizontal overflow). The top nav collapses into a hamburger drawer — brand, search, and avatar stay in the bar; nav links + account actions slide in from the side. Detection is two-layered: responsive CSS as the backbone plus server-side user-agent detection (`lib/device.ts`) for decisions CSS can't make — so Canvas, the drag-and-drop investigation workspace, isn't mounted at all on a phone; it shows a tidy 'best on desktop' card and drops out of the mobile nav. Scope is the read-first flow (project list → dashboard → daily brief → log); Canvas and other pointer-heavy editors stay desktop-only by design. The security-critical `proxy.ts` was left untouched — detection lives in component-level header reads on the already-dynamic app pages.

## 2026-06-17 · Payments — Clerk Billing scaffold (Free $2 / Pro), upgrade in-app
Category: feature

Wired up the path to charge: subscriptions run on **Clerk Billing** (built on Stripe, native to the Clerk auth we already use). New `/app/billing` page renders Clerk's `<PricingTable />`, and a plan now maps to one product effect — the per-user monthly AI budget it lifts (**Free $2 → Pro $15**, both env-tunable). `runAICall` resolves the cap live from the user's subscription via `auth().has({ plan })`, so an upgrade takes effect immediately with no entitlement store or webhook sync. It's **inert until configured**: with no plans in the Clerk dashboard, everyone stays Free and nothing charges — the only remaining steps are the dashboard ones (`npx clerk enable billing --for users`, create the `pro` plan, connect Stripe), documented in `docs/features/billing.md`. The public `/pricing` page stays beta-free until you flip it. BYOK remains unmetered on any plan.

## 2026-06-17 · Per-user $2/month AI cap — the guardrail that makes open sign-up safe
Category: infra

Added a fourth cost-control layer: a hard **$2/user/month** cap on platform-paid AI, summed across ALL of a user's projects (`IRV_USER_USD_CAP`, default $2). Until now spend was bounded per-project ($15) and platform-wide ($200), but nothing capped a single *account* — so opening sign-up to everyone risked one person (or a script) spinning up many projects to multiply cost. Now any user-attributed AI call checks the per-user month-to-date spend first and returns 402 with a clear 'resets next month, or bring your own key' message once they cross $2. Cron surfaces (daily brief, weekly narrative) run without a user and stay under the per-project cap; BYOK calls are exempt (their bill). Enforced in the `runAICall` gateway before any Anthropic request, so a tripped cap costs $0.

## 2026-06-13 · Reliability — stop dropping post-response work on Vercel (after() sweep)
Category: infra

On Vercel a function instance freezes the moment its response returns, so `void`-fired promises started after the response are silently dropped — the same failure mode that briefly killed webhook deliveries (fixed in the earlier closed-loop work). A sweep found more of the pattern and fixed it: the GitHub push pipeline (re-analysis + LLM call), the PR-bot comment, the auto-instrument PR, the manual re-analyze endpoint, and the repo-link flow now run inside `after()`; the AI **spend ledger** (`lib/ai.ts`) — which the per-project and platform cost caps count against — is now awaited so spend can't be undercounted; and the analytics daily-metrics write moved into `after()` (it claims an idempotency marker before writing, so a dropped write left a permanently-missing day). A new guard test fails CI if any heavy or correctness-critical helper is `void`-fired again.

## 2026-06-10 · Webhooks + mark_shipped — the closed loop: Irv detects, your agent fixes, Irv measures
Category: feature

Agents no longer poll. `POST /api/v1/subscriptions` registers a URL; Irv POSTs to it the moment a new critical insight or anomaly lands — HMAC-signed (X-Irv-Signature, same convention as GitHub webhooks), content-fingerprinted so re-refreshes don't re-fire, auto-disabled after 10 dead deliveries. Payloads carry the paste-ready `lovable_prompt` fix. On the act side, the MCP server gains its first write tool: `mark_shipped` records the fix, snapshots the cited metric, and starts the 7/14-day outcome clock (requires a write-scope token). The full circle is documented as recipe §9E in irv.dev/agents: subscribe → wake → fetch → ship fix → mark shipped → Irv measures the lift → agent reports what it actually moved. A self-healing app, with the human's role collapsed to reviewing the PR.

## 2026-06-09 · Agent observability — email pings + recent panel + 24h freshness chips
Category: feature

Three additions so agent signups don't land silently. New `lib/agent-provisions.ts` append-only log records each provision (agent_id, project_id, beta_key_label, contact_email, created_at) — last 100 rows kept. New `lib/admin-notifications.ts` fires a debounced email to creator emails on three events: agent applies, agent gets approved, agent provisions. Per-kind 10-minute debounce so a 50-event spike sends one email at t+0 saying '1 event' and another ~10 min later saying '47 since last batch'. Disable with `IRV_AGENT_SIGNUP_NOTIFY=0`. The `/app/admin/agent-beta` page gains a 24h counter chip + a 'Recent provisions' panel (last 20 rows, each linking to the provisioned project). `/app/admin/waitlist` gains '+N agents in 24h' chips in the header. All three layers fail independently — if Resend's down, the log + chips still work; if Redis hiccups on the log, email + chips still work.

## 2026-06-09 · Agents discoverability — `/agents` in site footer + docs hub callout
Category: polish

Site footer gains a 'For agents' link to `/agents` (between Docs and Pricing — visible on every page). The `/docs` hub leads with a pinned callout above the TOC: 'If you're here on behalf of an AI agent, hand it irv.dev/agents instead'. The `/agents` URL itself was shipped earlier; this PR just makes it findable from anywhere in the marketing surface. The companion instant-try endpoint that was prototyped alongside this work was held back pending beta launch — see `docs/features/agent-instant-try.md` for the deferred design + threat model.

## 2026-06-09 · Canonical agent entry URL — `irv.dev/agents`
Category: feature

The comprehensive agent reference now lives at `/agents` (canonical, recitable in conversation) as well as `/llms-full.txt` (the `llms.txt`-convention alias). Same bytes, two URLs — both pull from `src/lib/agent-reference.ts`. The short `/.well-known/llms.txt` summary now directs agents to `/agents` first. A human can tell their Claude or Codex "go visit irv.dev/agents" and the agent has everything in one fetch: how to sign up (three paths), how to integrate, how to pull data, the error model, copy-paste recipes. The `/docs/agents` page now opens with a callout pointing humans at `/agents` for the agent-readable form.

## 2026-06-09 · Agent reference — one canonical doc covering sign-up, integration, every endpoint
Category: feature

Rewrote `/llms-full.txt` from a marketing-led structure to an agent-API-led one. The single fetch now covers: three sign-up paths (autonomous beta-key, hand-reviewed application, device-code), identity verify, project setup, three integration paths (snippet + npm + direct ingest), every read endpoint (freshness, live, sessions, analytics, insights, daily brief, weekly narrative, outcomes, causation, ask), error model with codes per status, four copy-paste agent recipes, alt transports (CLI + MCP), and the last 30 changelog entries. The short `/.well-known/llms.txt` now explicitly directs agents to `/llms-full.txt` as the primary read.

## 2026-06-09 · Agent self-provisioning — one call to onboard an agent end-to-end
Category: feature

Closes the autonomy gap left by the agent application flow. `POST /api/v1/agent-beta/provision` takes a beta key (admin-minted at `/app/admin/agent-beta`, separate from PATs) and returns a synthetic agent identity, a fresh project + project_key, and a read+write PAT in one shot. No browser, no Clerk sign-up, no human in the loop. Companion `/api/v1/projects/{id}/events-freshness?since=2m` returns one number an agent can poll while verifying the snippet it just embedded is firing. Operator quickstart at `/docs/agents/quickstart` has copy-paste prompts for Codex CLI and Claude CLI — the agent can go provision → embed snippet → fire test event → verify → hand off in one continuous run.

## 2026-06-09 · Agent beta — apply at /agent, auto-mint token on first sign-in
Category: feature

A dedicated funnel for agent operators (Claude Desktop, Codex, custom workflows) to apply for Irv API access — separate from the human waitlist. Full intake form captures contact email, agent name, use case, scopes wanted, expected volume, and what built it. Admin sees agents in a new Humans/Agents toggle at /app/admin/waitlist with structured metadata at a glance. Approval sends an agent-flavoured invite email; on first /app/account visit a PAT is auto-minted with the requested scopes and surfaced once in a banner — operator copies it, wires it up, done. Companion changes: monochromatic light theme — orange + amber tokens dropped sitewide.

## 2026-06-04 · Waitlist admin — search, pagination, delete
Category: polish

Status filter chips compose with a debounced search box (matches email + the "what are you building" field, case-insensitive). 20 entries per page with a "1-20 of N" indicator. Per-row trash icon, confirm before firing — cleans every secondary index in one Upstash pipeline. The list stopped scrolling well around 30 signups; now scales cleanly.

## 2026-06-03 · Phone-verify gate + Claude-as-actor smoke tests
Category: security

Two halves of the same problem. Bot-attractive moments (PAT mint + device-code approve) gate behind a verified phone when `IRV_REQUIRE_PHONE_VERIFY=1` — using Clerk's native phone factor, no Twilio plumbing on our side. Default off; flip when abuse appears. Companion: `@irv/agent-tests`, an end-to-end suite where a real Claude agent drives Irv's API via tools across 5 scenarios. Catches integration bugs unit tests can't.

## 2026-06-03 · Test mode goes inline — banner follows you to the page being tested
Category: feature

The old test-mode page asked you to read a test, switch tabs, navigate to the surface, come back, mark status. Most tests are *about* a page so it didn't work. New flow: pick a project, click Start on any row, app navigates to the right page, a floating banner rides along with Pass / Fail / Skip / N/A buttons + a note field, Done returns to the overview. 45 of 50 test items have navigable destinations.

## 2026-06-03 · Agents-first API — PATs over HTTP, OpenAPI spec, device-code auth
Category: feature

Irv is now usable by any agent — Claude (API or Desktop), Codex, OpenAI function-calling, n8n, custom Node — not just MCP-aware clients. Mint a Personal Access Token at `/app/account`, pick scopes (`read` / `write` / `admin`), use the same token over both `/api/mcp` and `/api/v1/*`. New `/api/v1/me`, `/api/openapi.json`, `/.well-known/llms.txt`, and `/docs/agents` with code samples. Plus a full OAuth-style device-code flow at `/api/v1/auth/device` — agent shows you a code, you visit `/device`, click Approve, agent has a token. Same pattern as GitHub / Vercel CLI auth.

## 2026-06-02 · Canvas · zoom out to 10% + Fit-to-content (F key)
Category: polish

Zoom floor was 30% — too tight to see boards with widgets pushed into the corners. Now 10%. New `Maximize` button in the toolbar (also press **F**) computes the bounding box of every widget and zooms to show them all with a 40 px margin. Pure `fitToContent()` helper with 7 unit tests.

## 2026-06-02 · Feedback signal alerts — email when a high-severity pattern appears
Category: ai

The daily cron diffs the freshly-computed signals against the prior run, emails every creator address when a new severity ≥4 signal appears (cluster, spike, trend). Re-running the cron same day doesn't re-alert because the `(feature, signal_type)` keys haven't changed. Gated by `feedback-signals:alert-emails`, default off. Closes the loop — the panel stops needing to be checked.

## 2026-06-02 · Feedback system v2 — feature flags, AI signals, in-app helper
Category: feature

Three layers on top of the per-feature `?` chip. (1) Generic feature-flag system controls who sees each chip — `creator` / `members` / `all` / `off` — default `creator` so nothing leaks. (2) A daily cron runs both heuristic (cluster ≥3, spike ≥2.5×) and LLM passes over the feedback corpus, surfaces signals at the top of `/app/admin/feedback`. (3) `/app/help/feedback` user-facing explainer + first-time coachmark next to the first chip you see.

## 2026-06-01 · Test Mode + per-feature feedback chip
Category: feature

Two surfaces for the same loop. Creator-only `/app/test-mode` walks every shipped feature methodically — ~50 items grouped into 6 passes, Pass / Fail / Skip / N/A, notes, filter chips, markdown export. State persists per-user in Upstash. Companion: tiny `?` `<FeedbackChip feature="...">` next to every feature headline. Tap → popover with Works / Broken / Confusing → submits to the admin inbox tagged with the feature. Catches passive feedback you'd otherwise just think to yourself.

## 2026-05-31 · Magic Wand streaming — title in 500 ms, summary widget on complete
Category: ai

Same partial-JSON streaming pattern as the daily brief, applied to the Magic Wand canvas analysis. Select 2+ widgets → Analyze. Toolbar progress pill appears immediately; the title drops in ~500 ms; the summary widget materialises on complete. No more 5-10 s of staring at a spinner.

## 2026-05-30 · Daily Brief streams — headline in 500 ms instead of 5-10 s
Category: ai

Regenerating the brief used to mean ~5-10 s of waiting for the whole thing. Now uses Anthropic's `stream: true` + `input_json_delta` events + `partial-json` over the accumulating buffer to emit each section as soon as the model has "moved on" (the field is followed by structural punctuation). The headline appears in ~500 ms; sections drop in as they parse. Same six-section structure, same voice rules.

## 2026-05-30 · Prompt caching across every AI surface
Category: ai

Added Anthropic's `cache_control: ephemeral` to the system prompts on the four uncached surfaces (canvas chat, ask Irv, magic wand, weekly narrative — daily brief + insights were already cached). Repeat calls within the 5-min TTL hit the cache for ~80 % off the input-token cost. A meaningful spend reduction at zero UX cost.
