---
phase: 03-public-launch
plan: 03
subsystem: pricing-tier-monetization
tags: [mon-01, mon-02, pricing, tier-gate, require-tier, monetization-trigger]
type: execute
wave: 1
depends_on: []
files_modified:
  - app/lib/org.js
  - app/lib/repositories/monetization.repository.js
  - app/api/monetization/verified-integrations-count/route.js
  - middleware.js
  - app/pricing/page.js
  - .planning/PROJECT.md
  - README.md
  - __tests__/unit/require-tier.test.js
  - __tests__/unit/verified-integrations-count.route.test.js
  - __tests__/unit/monetization-repository.test.js
  - __tests__/unit/pricing-page.test.jsx
  - __tests__/unit/project-md-content.test.js
  - __tests__/unit/readme-monetization-trigger.test.js
  - __tests__/fixtures/pro-gated-route-fixture.js
autonomous: true
requirements: [MON-01, MON-02]
nyquist_compliant: true
wave_0_complete: true

user_setup: []  # Zero external services — pure composition of existing DB column + helpers

must_haves:
  truths:
    - "PROJECT.md canonically documents the monetization trigger: 50 verified Claude Code integrations in the wild"
    - "README.md contains a paragraph committing to the same trigger"
    - "Visiting /pricing renders a page stating the trigger commitment, current N/50 count, Free-forever scope, and what Pro will include when MON-01 fires"
    - "/api/monetization/verified-integrations-count GET returns {count: int, target: 50} — public, unauthenticated, no PII"
    - "requireTier('pro') helper in app/lib/org.js returns null for pro orgs and 403 {error, code: COMING_SOON, ...} for free orgs"
    - "requireTier response body contains NO buy / upgrade / subscribe / pay language — paywall is explicitly deferred (D-07)"
    - "Existing admin-gate at 15+ routes still works (non-regression) — requireTier is additive"
    - "No schema migration introduced — organizations.plan already exists at schema/schema.js:23 default 'free'"
  artifacts:
    - path: "app/lib/org.js"
      provides: "requireTier(request, minTier) helper alongside existing getOrgRole/getOrgId/getUserId"
      exports: ["requireTier"]
    - path: "app/lib/repositories/monetization.repository.js"
      provides: "countVerifiedIntegrations(sql, options) — SQL-encapsulated per route-sql guardrail"
      exports: ["countVerifiedIntegrations"]
    - path: "app/api/monetization/verified-integrations-count/route.js"
      provides: "Public GET endpoint returning {count, target: 50}"
      exports: ["GET"]
    - path: "middleware.js"
      provides: "PUBLIC_ROUTES updated to allowlist /api/monetization/verified-integrations-count"
      contains: "/api/monetization/verified-integrations-count"
    - path: "app/pricing/page.js"
      provides: "/pricing commitment page — trigger text, live N/50 counter, Free tier + Pro tier lists"
      exports: ["default"]
    - path: ".planning/PROJECT.md"
      provides: "Canonical monetization trigger commitment paragraph"
      contains: "50 verified Claude Code"
    - path: "README.md"
      provides: "Public monetization trigger paragraph visible on repo landing"
      contains: "50 verified Claude Code"
  key_links:
    - from: "app/pricing/page.js"
      to: "/api/monetization/verified-integrations-count"
      via: "server-side fetch OR direct repository call at render time"
      pattern: "countVerifiedIntegrations|verified-integrations-count"
    - from: "app/api/monetization/verified-integrations-count/route.js"
      to: "app/lib/repositories/monetization.repository.js"
      via: "countVerifiedIntegrations import"
      pattern: "from.*monetization.repository"
    - from: "app/lib/org.js (requireTier)"
      to: "app/lib/usage.js (getOrgPlan)"
      via: "plan column lookup"
      pattern: "getOrgPlan"
    - from: "middleware.js PUBLIC_ROUTES"
      to: "/api/monetization/verified-integrations-count"
      via: "array entry allowlist"
      pattern: "'/api/monetization/verified-integrations-count'"
---

<objective>
Ship the monetization trigger commitment publicly in 4 required locations (per D-03) and the `requireTier('pro')` middleware helper that designs the Pro tier boundary without shipping the paywall (per D-07). This is the MON-01 + MON-02 atomic deliverable.

**Purpose:** MON-01 makes "free first, paid later" have an actual *later*. A public commitment to "50 verified Claude Code integrations in the wild" in PROJECT.md + README.md + /pricing creates reputational cost to reneging. MON-02 ships the Pro-tier architecture dormant — `organizations.plan` column already exists, `getOrgPlan()` already queries it, `PLAN_LIMITS.pro` already has values; `requireTier` is ~15 lines composing these existing pieces. When MON-01 fires, flipping specific orgs to Pro is `UPDATE organizations SET plan='pro' WHERE id = ...` — zero code deploy.

**Output:**
- New `requireTier` helper in `app/lib/org.js` (MON-02)
- New `app/lib/repositories/monetization.repository.js` with `countVerifiedIntegrations` (repository-pattern per CLAUDE.md route-SQL guardrail)
- New public `GET /api/monetization/verified-integrations-count` → `{count, target: 50}`
- `middleware.js` PUBLIC_ROUTES updated to allowlist the counter route
- New `app/pricing/page.js` — one-paragraph commitment page with live N/50 counter
- Monetization trigger paragraph added to `.planning/PROJECT.md` (canonical — 3rd of 4 D-03 locations) and `README.md` (public GitHub landing — 4th of 4; locations 1 and 2 — /pricing and launch content — land here and in 03-02 respectively)
- Wave-0 tests + fixture route for requireTier regression coverage
- Assumption A8 resolved: agent_id ILIKE 'claude-code%' pattern grounded in `hooks/dashclaw_pretool.py:75` default
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/STATE.md
@.planning/phases/03-public-launch/03-CONTEXT.md
@.planning/phases/03-public-launch/03-RESEARCH.md
@.planning/phases/03-public-launch/03-VALIDATION.md
@CLAUDE.md
@.impeccable.md
@app/lib/org.js
@app/lib/usage.js
@schema/schema.js
@middleware.js
@hooks/dashclaw_pretool.py
@app/lib/repositories/actions.repository.js
@app/api/keys/route.js
@__tests__/unit/keys.route.test.js
@README.md

<interfaces>
<!-- Contracts executors use without exploring the codebase -->

From app/lib/org.js (EXISTING — extend alongside):
```javascript
export function getOrgId(request)  // returns string; default 'org_default'
export function getOrgRole(request) // returns 'admin' | 'member'; default 'member'
export function getUserId(request)  // returns string
```

From app/lib/usage.js (EXISTING — consume):
```javascript
export async function getOrgPlan(orgId, sql): Promise<string>
// Returns rows[0].plan || 'free' if org exists, else 'free'. Safe fallback guaranteed (A11 verified).

export const PLAN_LIMITS = { free: {...Infinity}, pro: {...Infinity}, business: {...}, enterprise: {...} }
// 'pro' key already exists at lib/usage.js:38-47
```

From schema/schema.js:23 (VERIFIED — organizations.plan):
```javascript
// organizations table includes:
plan: text('plan').notNull().default('free')
// text column, no CHECK constraint — 'pro' is valid value without migration
```

From hooks/dashclaw_pretool.py:75 (VERIFIED — A8 resolution source):
```python
AGENT_ID = os.environ.get("DASHCLAW_AGENT_ID") or "claude-code"
# Default "claude-code"; users CAN override with DASHCLAW_AGENT_ID=<custom>
# Canonical SQL match: agent_id ILIKE 'claude-code%' (catches default + any 'claude-code-X' override)
```

From middleware.js:27-42 (EXISTING — PUBLIC_ROUTES array):
```javascript
const PUBLIC_ROUTES = [
  '/api/health',
  '/api/setup/status',
  '/api/setup/proof',
  '/api/setup/ping',
  '/api/setup/migrate',
  '/api/auth',
  '/api/cron',
  '/api/telegram/webhook',
  '/api/discord/interactions',
  '/api/docs/raw',
  '/api/prompts',
  '/practical-systems',
  '/replay',
  // ADD: '/api/monetization/verified-integrations-count'
];
```

Canonical admin-gate pattern (from app/api/keys/route.js:58 + 14 other sites — mirror exactly):
```javascript
if (getOrgRole(request) !== 'admin') {
  return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
}
```

Test pattern to mirror (from __tests__/unit/keys.route.test.js:62-96):
```javascript
// Mock sql + helper via vi.hoisted + vi.mock
// makeRequest helper from __tests__/helpers.js
// Assert response.status + body content + negative assertions on forbidden CTAs
```

**requireTier contract (new):**
```javascript
/**
 * @param {Request} request - Next.js request with x-org-id header (middleware-injected)
 * @param {string} minTier - Minimum required tier ('pro' only in Phase 3)
 * @returns {Promise<null | NextResponse>}
 *   - null: org meets minTier; caller proceeds
 *   - NextResponse 403 with body:
 *     {
 *       error: 'Coming soon',
 *       code: 'COMING_SOON',
 *       reason: string (contains '50 verified' + '/pricing', NO buy/upgrade/subscribe/pay),
 *       current_tier: string,
 *       required_tier: string
 *     }
 */
export async function requireTier(request, minTier): Promise<null | NextResponse>
```

**Verified-integrations counter contract (new):**
```javascript
// GET /api/monetization/verified-integrations-count
// Public, unauthenticated (PUBLIC_ROUTES), rate-limited by middleware
// Success: 200 { count: int, target: 50 }
// Failure (DB unavailable): 200 { count: null, target: 50, error: 'unavailable' }
//   ^ fails graceful — counter is signal, not critical infra
```

**Repository contract (new):**
```javascript
// app/lib/repositories/monetization.repository.js
// @param {object} sql - Neon SQL driver (getSql() result)
// @param {object} options - { excludeOrgIds?: string[], recencyDays?: number }
// @returns {Promise<number>} count of distinct orgs with ≥1 claude-code action_record in the recency window
export async function countVerifiedIntegrations(sql, options = {}): Promise<number>
```
</interfaces>

<assumption_resolution>
**A1 (blog location):** Not relevant to Plan 03-03. Blog is 03-02's concern. Plan 03-01 confirmed blog lives in this monorepo (no sibling dashclaw-website repo exists).

**A8 (agent_id pattern) — RESOLVED:**
- Inspected `hooks/dashclaw_pretool.py:75`: `AGENT_ID = os.environ.get("DASHCLAW_AGENT_ID") or "claude-code"`.
- Default is `"claude-code"`. Users MAY override with `DASHCLAW_AGENT_ID=<custom>`.
- **Canonical SQL:** `agent_id ILIKE 'claude-code%'` — catches the default and common overrides like `claude-code-wes-laptop`.
- **Excluded org IDs (D-01 says "in the wild"):** `org_default` (founder's own instance), `org_demo` (demo sandbox). Repository accepts excludeOrgIds for configurability.
- **Recency window:** 90 days — prunes stale one-off integrations. Founder-verifiable: Wes can `SELECT DISTINCT org_id, agent_id FROM action_records WHERE agent_id ILIKE 'claude-code%' AND org_id NOT IN ('org_default','org_demo') AND timestamp_start > NOW() - INTERVAL '90 days' ORDER BY org_id` to eyeball the list.
- **Precision tradeoff:** Over-permissive (any agent_id matching claude-code%) is better than under-permissive here. Wes wants the counter to not under-count and trigger too late; if it over-counts, he'll spot obvious noise in manual verification before flipping PAY-01.

**A11 (getOrgPlan fallback) — VERIFIED:** `app/lib/usage.js:81-83` returns `rows.length > 0 ? (rows[0].plan || 'free') : 'free'` — safe for any `text` value in the plan column, including NULL and unknown values.
</assumption_resolution>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: requireTier helper + Wave-0 test scaffolds (RED → GREEN in one task — small, isolated unit)</name>
  <files>app/lib/org.js, __tests__/unit/require-tier.test.js, __tests__/fixtures/pro-gated-route-fixture.js</files>
  <read_first>
    - `app/lib/org.js` (existing getOrgId/getOrgRole/getUserId — extend alongside, do not refactor)
    - `app/lib/usage.js` lines 30-85 (PLAN_LIMITS and getOrgPlan signature — exact import path)
    - `__tests__/unit/keys.route.test.js` lines 1-100 (canonical vitest + vi.hoisted + vi.mock pattern — mirror exactly)
    - `__tests__/helpers.js` (makeRequest helper signature — used to synthesize Request objects)
    - `app/api/keys/route.js:58` (canonical admin-gate pattern; requireTier is its sibling, not replacement)
    - `.planning/phases/03-public-launch/03-RESEARCH.md` §Code Examples (requireTier reference implementation + test pattern)
    - `.planning/phases/03-public-launch/03-CONTEXT.md` D-04 (single paid tier Free+Pro), D-05 (Free forever scope), D-06 (Pro features), D-07 (requireTier middleware, NOT /pro/* tree, NOT separate package)
  </read_first>
  <behavior>
    **requireTier contract (MUST match these test cases exactly):**

    Case 1: Pro org requesting pro-gated → returns null
    - mockGetOrgPlan returns 'pro'
    - requireTier(request, 'pro') → null
    - Caller proceeds to route handler

    Case 2: Free org requesting pro-gated → returns 403 COMING_SOON (with positive D-03 commitment signal)
    - mockGetOrgPlan returns 'free'
    - requireTier(request, 'pro') → NextResponse status 403 body
      { error: 'Coming soon',
        code: 'COMING_SOON',
        reason: <string>,
        current_tier: 'free',
        required_tier: 'pro' }
    - **Positive assertions (required — not just absence):**
      - `expect(body.code).toBe('COMING_SOON')`
      - `expect(body.reason).toContain('50 verified')` — D-03 commitment signal present in the 403 body
      - `expect(body.reason).toContain('/pricing')` — points at the live counter page
      - `expect(body.current_tier).toBe('free')` + `expect(body.required_tier).toBe('pro')`
    - Rationale: without positive assertions on the commitment signal, someone could rewrite the reason field to something like "pro feature, coming soon" and pass the test while silently dropping the D-03 commitment surface. The trigger phrase + /pricing link must be non-negotiable in the 403 body.
    - Response status 403

    Case 3: Unknown plan value (NULL or 'mystery-tier') → falls back to free-tier behavior
    - mockGetOrgPlan returns null (as getOrgPlan's fallback does on no-row)
    - requireTier(request, 'pro') → 403 COMING_SOON (default-deny posture — T-03-03-01 mitigation)
    - current_tier in response is the raw string (null/unknown) — surface truthfully

    Case 4: NO buy/upgrade/subscribe/pay language in response body
    - Negative regex on response.reason: NOT /buy|upgrade|subscribe|pay/i
    - This is explicit per D-07 (paywall deferred; response is Coming Soon, not a CTA)

    Case 5 (non-regression fixture): admin-gate at app/api/keys/route.js still works
    - Existing keys.route.test.js suite passes unchanged
    - requireTier is additive, not replacement
  </behavior>
  <action>
    1. **Extend `app/lib/org.js`**: append `requireTier` function AFTER the existing helpers. Do NOT refactor existing exports. Implementation per RESEARCH §Code Examples (15 lines):
       ```javascript
       import { NextResponse } from 'next/server';
       import { getSql } from './db.js';
       import { getOrgPlan } from './usage.js';

       const TIER_RANK = { free: 0, pro: 1 };

       export async function requireTier(request, minTier) {
         const orgId = getOrgId(request);
         const sql = getSql();
         const currentTier = await getOrgPlan(orgId, sql);
         const currentRank = TIER_RANK[currentTier] ?? 0;
         const requiredRank = TIER_RANK[minTier] ?? 0;
         if (currentRank >= requiredRank) return null;
         return NextResponse.json({
           error: 'Coming soon',
           code: 'COMING_SOON',
           reason: 'Pro features unlock when DashClaw hits 50 verified Claude Code integrations in the wild. See /pricing for progress.',
           current_tier: currentTier,
           required_tier: minTier,
         }, { status: 403 });
       }
       ```
       (Adjust import ordering to match existing file's style; do not reorder existing imports.)

    2. **Create `__tests__/fixtures/pro-gated-route-fixture.js`** — a test fixture route that consumes requireTier:
       ```javascript
       import { NextResponse } from 'next/server';
       import { requireTier } from '../../app/lib/org.js';

       export async function GET(request) {
         const tierBlock = await requireTier(request, 'pro');
         if (tierBlock) return tierBlock;
         return NextResponse.json({ ok: true });
       }
       ```
       Fixture ships as test-only scaffolding so requireTier has a callsite to exercise against. NO actual Pro feature ships.

    3. **Create `__tests__/unit/require-tier.test.js`** per the 5 behavior cases above. Mirror `__tests__/unit/keys.route.test.js:62-96` vi.hoisted + vi.mock pattern exactly. Import path aliases: use `@/lib/org.js`, `@/lib/usage.js`, `@/lib/db.js` matching the project's vitest config.

    4. Run: `npm test -- --run __tests__/unit/require-tier.test.js` — must pass all 5 cases.
    5. Commit: `feat(03-03): add requireTier middleware helper + pro-gated fixture (MON-02)`

    **Threat mitigation:** T-03-03-01 (fail-open) addressed — unknown plan values default to rank 0 (free), never rank > 0 (pro). Default-deny posture tested explicitly in case 3.
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/require-tier.test.js 2>&1 | tail -20</automated>
  </verify>
  <done>
    - `app/lib/org.js` exports requireTier alongside existing helpers
    - `__tests__/fixtures/pro-gated-route-fixture.js` exists — consumes requireTier
    - `__tests__/unit/require-tier.test.js` — 5 test cases all PASS
    - Case 2 positive assertions land: `body.code === 'COMING_SOON'`, `body.reason` contains BOTH `'50 verified'` AND `'/pricing'`, `body.current_tier === 'free'`, `body.required_tier === 'pro'` — not just absence of CTA language
    - Negative assertion: response body contains NO buy/upgrade/subscribe/pay language
    - Default-deny test: unknown plan falls back to free-tier 403
    - Commit lands as `feat(03-03): add requireTier middleware helper + pro-gated fixture (MON-02)`
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Verified-integrations counter repository + public API route + PUBLIC_ROUTES allowlist</name>
  <files>app/lib/repositories/monetization.repository.js, app/api/monetization/verified-integrations-count/route.js, middleware.js, __tests__/unit/monetization-repository.test.js, __tests__/unit/verified-integrations-count.route.test.js</files>
  <read_first>
    - `app/lib/repositories/actions.repository.js` (existing repository pattern — exports + sql-tagged-template style)
    - `app/lib/repositories/index.js` (central export file — may need to re-export countVerifiedIntegrations if that's the established pattern)
    - `app/api/health/route.js` (simplest existing public GET — structure mirror)
    - `middleware.js:27-42` (PUBLIC_ROUTES array to extend)
    - `middleware.js:1118-1135` (rate-limit path handling for PUBLIC_ROUTES — confirms new route still gets rate-limited; no special handling needed)
    - `hooks/dashclaw_pretool.py:75` (A8 source — AGENT_ID default "claude-code")
    - `.planning/phases/03-public-launch/03-RESEARCH.md` §Pattern 4 (counter SQL recipe), §Pitfall 6 (route-SQL guardrail — must use repository, NOT raw SQL in route)
    - `CLAUDE.md` §Route SQL guardrail (no direct SQL in route files)
    - `MEMORY.md` §SDK Documentation Checklist — this task adds a new API route → must update 6 docs (app/docs/page.js, sdk/README.md, sdk-python/README.md, docs/sdk-parity.md, docs/api-inventory.md, PROJECT_DETAILS.md) + run auto-regen commands
  </read_first>
  <behavior>
    **Repository (countVerifiedIntegrations) behavior:**

    Case 1: Empty DB (no action_records) → returns 0
    Case 2: DB with 3 distinct orgs where agent_id = 'claude-code' (default) within 90 days, excluding org_default + org_demo → returns 3
    Case 3: DB with 1 org where agent_id = 'claude-code-wes-laptop' (override) → returns 1 (ILIKE 'claude-code%' matches)
    Case 4: DB with 1 org where agent_id = 'other-agent' → returns 0 (doesn't match prefix)
    Case 5: DB with 1 org matching pattern but in excluded list (org_default) → returns 0
    Case 6: DB with 1 org matching pattern but outside recency window (>90 days) → returns 0
    Case 7: Signature: `countVerifiedIntegrations(sql, { excludeOrgIds?: string[], recencyDays?: number })` — defaults: excludeOrgIds=['org_default','org_demo'], recencyDays=90

    **Route (/api/monetization/verified-integrations-count) behavior:**

    Case 1: GET → 200 { count: <integer>, target: 50 }
    Case 2: Repository throws (DB unavailable) → 200 { count: null, target: 50, error: 'unavailable' } (fail-graceful — counter is signal, not critical infra)
    Case 3: Response does NOT include org_id, agent_id, or any per-org data — T-03-03-02 mitigation (information-disclosure — only aggregate)
    Case 4: Route is public (no auth required) → middleware.js PUBLIC_ROUTES allowlist includes this path
    Case 5: POST / PUT / DELETE to the endpoint → 405 Method Not Allowed (or Next.js default — only GET exported)

    **Middleware (PUBLIC_ROUTES) behavior:**

    Case 1: Unauthenticated GET to /api/monetization/verified-integrations-count → reaches the handler (PUBLIC_ROUTES bypasses API-key auth)
    Case 2: Still rate-limited (per middleware.js:1092-1093 — PUBLIC_ROUTES doesn't bypass rate limiting)
  </behavior>
  <action>
    1. **Create `app/lib/repositories/monetization.repository.js`**:
       ```javascript
       /**
        * Monetization repository — counter for MON-01 "verified integrations" trigger.
        * Encapsulates SQL per CLAUDE.md route-SQL guardrail.
        */

       export async function countVerifiedIntegrations(sql, options = {}) {
         const excludeOrgIds = options.excludeOrgIds ?? ['org_default', 'org_demo'];
         const recencyDays = options.recencyDays ?? 90;
         const rows = await sql`
           SELECT COUNT(DISTINCT org_id)::int AS count
           FROM action_records
           WHERE agent_id ILIKE 'claude-code%'
             AND org_id <> ALL(${excludeOrgIds})
             AND timestamp_start > NOW() - (${recencyDays} || ' days')::interval
         `;
         return rows[0]?.count ?? 0;
       }
       ```
       (Adjust SQL-driver parameterization to match Neon's `sql` tagged-template conventions used in `app/lib/repositories/actions.repository.js` — e.g., array binding and interval casting may need adjustment. Executor verifies against existing repository patterns.)

       If `app/lib/repositories/index.js` re-exports all repositories, add `export * from './monetization.repository.js'` there too.

    2. **Create `app/api/monetization/verified-integrations-count/route.js`**:
       ```javascript
       import { NextResponse } from 'next/server';
       import { getSql } from '../../../lib/db.js';
       import { countVerifiedIntegrations } from '../../../lib/repositories/monetization.repository.js';

       export const dynamic = 'force-dynamic';
       // Note: runtime revalidation via ISR is acceptable if Next.js fetch caching is used;
       // for now use dynamic to keep counter fresh. Planner-level revisit if cost spikes.

       export async function GET() {
         try {
           const sql = getSql();
           const count = await countVerifiedIntegrations(sql);
           return NextResponse.json({ count, target: 50 });
         } catch (e) {
           return NextResponse.json({ count: null, target: 50, error: 'unavailable' }, { status: 200 });
         }
       }
       ```
       **Critical:** route file contains ZERO raw SQL — all SQL through the repository. Confirms `npm run route-sql:check` stays at baseline.

    3. **Update `middleware.js:27-42`**: Add exactly one line to PUBLIC_ROUTES array: `'/api/monetization/verified-integrations-count'`. Insert in alphabetical or semantic group (after existing public read-only entries).

    4. **Create `__tests__/unit/monetization-repository.test.js`** — mock sql tagged-template to return canned rows per 7 repository cases. Pattern mirror from any existing repository test if one exists; otherwise straightforward vitest + vi.fn mocks.

    5. **Create `__tests__/unit/verified-integrations-count.route.test.js`** — mock getSql + countVerifiedIntegrations. Cover the 5 route behavior cases. Negative assertion: response body does NOT contain `org_id`, `agent_id`, or any per-org data keys (T-03-03-02).

    6. **SDK Docs Checklist** (per MEMORY.md §SDK Documentation Checklist — adding new API route triggers this):
       - Check whether this counter route should appear in `app/docs/page.js` (website docs). **Recommendation:** YES — it's public + documented surface. Add a minimal entry under a "Monetization" section OR omit if the docs page scope is SDK-only (executor verifies current docs scope).
       - Check `sdk/README.md` + `sdk-python/README.md` — counter is a one-off public GET, not an SDK method. **Recommendation:** SKIP (not an SDK method; SDKs don't expose monetization data to user agents).
       - Check `docs/sdk-parity.md` — same reasoning; SKIP if it's a method-count matrix only.
       - Run `npm run api:inventory:generate` — adds the route to `docs/api-inventory.md` + `docs/api-inventory.json` automatically. Commit the regenerated diff.
       - Run `npm run openapi:generate` — adds to `docs/openapi/critical-stable.openapi.json` if the generator scans for public routes. Commit the regenerated diff.
       - Confirm `npm run docs:check` + `npm run api:inventory:check` + `npm run openapi:check` all pass post-regen.

    7. **Livingcode auto-regen** (per CLAUDE.md §Generated Artifacts — we're touching `app/api/` + `app/lib/` + `middleware.js`, so pre-commit hook will trigger `npm run livingcode:refresh`): expect staged changes in `app/lib/doctor/generated/`, `public/livingcode/index.html`, `public/downloads/dashclaw-platform-intelligence*`, `docs/api-inventory.md`. Commit these alongside the code — NEVER hand-edit.

    8. Verify:
       ```bash
       npm test -- --run __tests__/unit/monetization-repository __tests__/unit/verified-integrations-count
       npm run route-sql:check            # baseline 85 held — repository encapsulates SQL, route file has zero raw SQL
       npm run openapi:check              # regenerated artifacts match
       npm run api:inventory:check        # regenerated artifacts match
       npm run docs:check
       ```

    9. Commit: `feat(03-03): verified-integrations counter API + repository (MON-01)` — bundle code + tests + regenerated artifacts.

    **Threat mitigation:** T-03-03-02 (information disclosure) addressed by route response shape being aggregate-only; route test asserts no per-org keys. Route-SQL guardrail held by repository encapsulation (Pitfall 6).
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/monetization-repository __tests__/unit/verified-integrations-count && npm run route-sql:check && npm run openapi:check && npm run api:inventory:check && npm run docs:check</automated>
  </verify>
  <done>
    - `app/lib/repositories/monetization.repository.js` exports countVerifiedIntegrations; SQL uses `agent_id ILIKE 'claude-code%'` + excludeOrgIds + recencyDays
    - `app/api/monetization/verified-integrations-count/route.js` exists; route file contains ZERO raw SQL
    - `middleware.js` PUBLIC_ROUTES includes `/api/monetization/verified-integrations-count`
    - Both test files PASS (7 repository cases + 5 route cases)
    - Route response body asserted to contain NO org_id, agent_id, or per-org data
    - Route-SQL guardrail baseline held (85 or lower)
    - openapi:check, api:inventory:check, docs:check all pass
    - Livingcode auto-regen artifacts committed alongside code change
    - Commit lands as `feat(03-03): verified-integrations counter API + repository (MON-01)`
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: /pricing commitment page + PROJECT.md + README.md trigger commitments (D-03 four-location lock)</name>
  <files>app/pricing/page.js, .planning/PROJECT.md, README.md, __tests__/unit/pricing-page.test.jsx, __tests__/unit/project-md-content.test.js, __tests__/unit/readme-monetization-trigger.test.js</files>
  <read_first>
    - `.planning/PROJECT.md` (current — find the appropriate section for the trigger paragraph — likely Active Requirements or Key Decisions table; the "Monetization: free first, paid later" row at line 81 needs updating from ⚠️ Trigger pending → ✓ Locked)
    - `README.md` (current — find a spot for the trigger paragraph; likely after the 3-step install block, before the fold)
    - `app/globals.css` (CSS tokens — use exclusively; NO hardcoded hex)
    - `.impeccable.md` (design gate — brand orange on the live counter NUMBER only, per RESEARCH anti-pattern)
    - `app/page.js` (token patterns — match style for consistency)
    - `app/connect/page.js` (SSR page structure mirror — PublicNavbar/PublicFooter shell)
    - `.planning/phases/03-public-launch/03-RESEARCH.md` §Pattern 3 (recommended /pricing structure — single column, token-based, terse)
    - `.planning/phases/03-public-launch/03-CONTEXT.md` D-01 (trigger = 50 verified CC integrations), D-03 (4 public locations), D-04/D-05 (Free forever scope), D-06 (Pro when fires)
    - `app/lib/repositories/monetization.repository.js` (Task 2 deliverable — called server-side from /pricing for live counter)
    - `scripts/check-readme-lead.mjs` (README lead must still pass — first 50 lines unchanged)
  </read_first>
  <behavior>
    **`/pricing` page (app/pricing/page.js) behavior:**
    - Test: rendered HTML contains "50 verified Claude Code integrations" (exact trigger commitment)
    - Test: rendered HTML contains "Free forever" AND all 5 of 5 Free tier bullets per D-05 (locked decision — not a 3-of-5 threshold):
      ```javascript
      const freeBullets = [
        'solo-dev Claude Code integration',
        'Discord',              // Discord + Telegram approvals bullet — substring
        'Telegram',             // same bullet — split across two substring matches for robustness
        '/decisions ledger',
        'semantic guard',
        '/activity',
        '/my-agent',
      ];
      // The 7 substrings above collapse to 5 distinct D-05 bullets.
      // Require all 5 distinct bullets present. Executor picks assertion strategy
      // (e.g., match each substring → count unique bullets covered → expect 5).
      expect(matchedBullets.length).toBe(5);  // NOT .toBeGreaterThanOrEqual(3)
      ```
    - Test: rendered HTML contains "Pro" tier section with ALL 4 of 4 Pro features per D-06 (locked decision — not a 3-of-4 threshold): multi-user orgs + SSO, custom policy pack authoring, audit export, non-Claude-Code integrations. `expect(matchedProBullets.length).toBe(4);` — NOT `.toBeGreaterThanOrEqual(3)`.
    - Test: live counter text present — rendered HTML matches `/\d+\s*\/\s*50/` (accepts both "7 / 50" and "7/50"; rejects literal placeholder "N/50"). Where N is a rendered integer from the server-side counter call. On counter error: fallback renders "—" (tested separately — does NOT need to match the /\d+\s*\/\s*50/ regex in the error fallback case).
    - Test: negative assertion — NO paywall language in rendered HTML: /buy|upgrade now|subscribe|pay now|purchase|checkout/i returns zero matches (NOT buy-CTA per D-07)
    - Test: brand orange used sparingly — single element with `text-brand` class (likely the counter number); full page body does NOT use `bg-brand` (orange-as-ambient-wallpaper anti-pattern per RESEARCH)

    **PROJECT.md behavior:**
    - Test: file contains the string "50 verified Claude Code integrations" (canonical — D-03 location 3)
    - Test: the "Monetization: free first, paid later — but with a trigger" row in Key Decisions table is updated from `⚠️ Trigger pending` to `✓ Locked`

    **README.md behavior:**
    - Test: file contains the string "50 verified Claude Code integrations" (D-03 location 4 — GitHub repo landing)
    - Test: `scripts/check-readme-lead.mjs` still exits 0 (first 50 lines preserved — trigger paragraph goes AFTER line 50, not replacing the lead)
    - Test: trigger phrase first appears at line number > 50 (intra-wave parallel safety — Plan 03-01 Task 4 owns README lines 8 and 19)
  </behavior>
  <action>
    1. **Create `app/pricing/page.js`** — SSR page. Structure per RESEARCH §Pattern 3:
       - PublicNavbar + PublicFooter shell (mirror app/connect/page.js pattern)
       - Single column, max-width container
       - Hero paragraph: "DashClaw is free while we grow."
       - Subhead: brief explanation that the runtime (hook, policy pack, Discord approvals, activity timeline, semantic guard) is free forever.
       - Trigger commitment block — larger, slightly stronger visually (but NOT orange background):
         > "Pro tier launches when DashClaw hits 50 verified Claude Code integrations in the wild."
       - Live counter block: render `N / 50` where `N` is fetched server-side. Two acceptable approaches:
         - (a) Call `countVerifiedIntegrations(getSql())` directly in the server component (no HTTP round-trip; just pull from repository)
         - (b) Server-side fetch from `/api/monetization/verified-integrations-count`
         Executor picks (a) for simplicity (no network hop). Use `export const dynamic = 'force-dynamic'` or `export const revalidate = 300` (5-min cache acceptable — counter changes slowly).
         On error: render "—" for N (fail-graceful).
         Brand orange on the counter NUMBER only: `<span className="text-brand">{count}</span>` — all other text uses `text-text-primary` / `text-text-secondary`.
       - Free-forever list (bullets from D-05): Solo-dev Claude Code integration · Discord + Telegram approvals · /decisions ledger · Semantic guard (bring your own LLM key) · /activity + /my-agent
       - Pro-when-fires list (bullets from D-06): Multi-user orgs + SSO + role-based policies · Custom policy pack authoring · Audit export + SOC-2-friendly reporting · Integrations beyond Claude Code (Cursor, Aider, Devin, custom SDK)
       - Closing paragraph (transparency posture): "This commitment also lives in our PROJECT.md, our README, and our launch tweet + HN post. If we renege, it costs reputation, not just a private retro."
       - Styling: CSS tokens ONLY. Single-column. Terse. No marketing decoration. Matches .impeccable.md voice.

    2. **Update `.planning/PROJECT.md`**:
       - Locate the `## Requirements > ### Active` section OR the `## Key Decisions` table — add a trigger commitment paragraph. Suggested placement: near the existing "**Define a monetization trigger**" Active requirement bullet (line ~42). Add explicit paragraph below or inline:
         > **Monetization trigger (locked):** Pro tier launches when DashClaw hits **50 verified Claude Code integrations in the wild**. Measured by a SQL query over `action_records` where `agent_id ILIKE 'claude-code%'` excluding internal orgs, with a 90-day recency window. No time-boxed backstop — the trigger fires when it fires. Committed publicly on dashclaw.io/pricing, README.md, and the launch tweet/HN post.
       - Update Key Decisions table row 81 (`Monetization: free first, paid later — but with a trigger`): change Outcome column from `⚠️ Trigger pending` to `✓ Locked`
       - Test assertion: `grep -F "50 verified Claude Code integrations" .planning/PROJECT.md` returns ≥1 match

    3. **Update `README.md`** — **insertion point: AFTER line 50** (explicit intra-wave-parallel safety — Plan 03-01 Task 4 also edits README lines 8 and 19 for screencast backfill; both plans are Wave 1 and same-wave plans must have zero overlapping line ranges):
       - Preserve first 50 lines unchanged (`scripts/check-readme-lead.mjs` gate — first-50-line assertion for "Claude Code" mention + `/guides/claude-code` link)
       - **Insertion point: AFTER line 50.** Preferred placement: a new section AFTER the existing `## Deploy` block (which currently ends around line 67 — executor verifies). Heading: `## Monetization & public commitment` (or `### Free while we grow` as a sub-section of Deploy — executor's judgment; key constraint is line number > 50 AND no conflict with any lines Plan 03-01 Task 4 touches, which are lines 8 and 19).
       - Body: one paragraph committing publicly to the trigger:
         > DashClaw is free while we grow. Pro tier launches when we hit **50 verified Claude Code integrations in the wild**. We'll track progress publicly at [dashclaw.io/pricing](https://dashclaw.io/pricing). The runtime — hook, policy pack, Discord approvals, audit ledger, semantic guard — stays free forever.
       - Test assertions:
         - `grep -F "50 verified Claude Code integrations" README.md` returns ≥1 match
         - `grep -n "50 verified Claude Code integrations" README.md` first match line number is > 50 (enforces the parallel-safety insertion rule)
         - `node scripts/check-readme-lead.mjs` still exits 0 (non-regression — first 50 lines unchanged)
       - **Why AFTER line 50 and not earlier:** Plan 03-01 Task 4 (same Wave 1, runs in parallel) edits README lines 8 and 19 to backfill screencast URLs. If Plan 03-03 Task 3 inserted the monetization paragraph at line ~35-39 as originally drafted, the two plans would collide in a git merge. After-line-50 insertion eliminates the collision window. The original claim that the insertion lives "after the first 50 lines" was weakened by an earlier draft that pointed at line ~35; this is the corrected pointer.

    4. **Create `__tests__/unit/pricing-page.test.jsx`** — 6 cases per behavior (trigger text + Free-forever bullets + Pro bullets + counter format + no-paywall negative + single-orange-element). Mock `getSql` + `countVerifiedIntegrations` to return a known integer (e.g., 7). Use RTL + rendered HTML string assertions.

    5. **Create `__tests__/unit/project-md-content.test.js`** — Node `readFileSync` on `.planning/PROJECT.md`; assert trigger text present; assert Key Decisions row updated.

    6. **Create `__tests__/unit/readme-monetization-trigger.test.js`** — Node `readFileSync` on `README.md`; assert trigger paragraph present; also shell out to verify `check-readme-lead.mjs` passes (or re-implement the check inline).

    7. Verify:
       ```bash
       npm test -- --run __tests__/unit/pricing-page __tests__/unit/project-md-content __tests__/unit/readme-monetization-trigger
       npm test -- --run   # full suite — non-regression
       node scripts/check-readme-lead.mjs
       ```

    8. **Livingcode auto-regen**: this task touches `app/pricing/` (new app route), so pre-commit hook will regenerate. Commit staged deltas alongside.

    9. Commit: `feat(03-03): /pricing commitment page + MON-01 trigger in PROJECT.md + README.md (MON-01)`

    **Threat mitigation:** T-03-03-02 (/pricing leaks per-org data) — the page renders ONLY the aggregate count; the repository returns an integer, not a row set. T-03-03-03 (pre-paywall messaging) — negative assertion on paywall language verifies "currently free for all" framing, not "upgrade to Pro".
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/pricing-page __tests__/unit/project-md-content __tests__/unit/readme-monetization-trigger && node scripts/check-readme-lead.mjs && npm test -- --run</automated>
  </verify>
  <done>
    - `app/pricing/page.js` exists; renders trigger commitment + N/50 counter + Free forever list + Pro when fires list + closing transparency paragraph
    - Pricing page rendered HTML contains NO paywall/buy/upgrade/subscribe language
    - Brand orange used on counter number only; no bg-brand on the page body
    - `.planning/PROJECT.md` contains "50 verified Claude Code integrations" + Key Decisions row updated to ✓ Locked
    - `README.md` contains "50 verified Claude Code integrations" in a paragraph inserted AFTER line 50 (first match line number > 50; preferred placement: new section after the `## Deploy` block)
    - `scripts/check-readme-lead.mjs` exits 0 (non-regression — first 50 lines preserved)
    - Intra-wave parallel safety held: Plan 03-01 Task 4 edits README lines 8 and 19 for screencast backfill; Plan 03-03 Task 3 edits are > line 50. Zero overlap — safe to run in parallel within Wave 1.
    - All 3 new test files PASS
    - Full suite `npm test -- --run` passes
    - Livingcode auto-regen artifacts committed alongside code
    - Commit lands as `feat(03-03): /pricing commitment page + MON-01 trigger in PROJECT.md + README.md (MON-01)`
    - D-03 four-location commitment: location 3 (PROJECT.md) + location 4 (README.md) + location 1 (/pricing) all landed by Plan 03-03. Location 2 (launch tweet + HN post) lands in Plan 03-02.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Public web → /api/monetization/verified-integrations-count | Unauthenticated endpoint — response must contain NO per-org data, only aggregate |
| Authenticated handler → requireTier → organizations.plan lookup | Handler-level tier gate; header-injection attack (x-org-id) already mitigated pre-Phase-3 by middleware.js:1076 stripping inbound x-org-* headers |
| Public web → /pricing → repository → action_records | Server component reads aggregate count; never returns per-org rows to the client |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-03-01 | Elevation of Privilege | requireTier fails open for unknown plan values | mitigate | `TIER_RANK[currentTier] ?? 0` defaults unknown tiers to rank 0 (free) — never rank > 0. Test case 3 asserts null/unknown plan → 403 COMING_SOON. Default-deny posture |
| T-03-03-02 | Information Disclosure | /pricing counter or API leaks per-org data | mitigate | Repository returns integer (COUNT DISTINCT). Route response shape asserted to contain NO org_id, agent_id, or per-org keys. Pricing page renders only the integer |
| T-03-03-03 | Reputational / UX | 403 response confuses users with buy-CTA | mitigate | D-07-compliant response body: reason text contains "Coming soon" + "50 verified" + "/pricing" — explicit negative regex on /buy|upgrade|subscribe|pay/i. Pricing page same — no paywall language |
| T-03-03-04 | Tampering | Header injection bypasses requireTier (client sends x-org-id: org_pro) | mitigate | middleware.js:1076 strips x-org-id/x-org-role/x-user-id from inbound API requests — only middleware-injected values reach handlers. Already shipped pre-Phase-3; verified via inherited test coverage |
| T-03-03-05 | Tampering | Route file contains raw SQL that bypasses repository pattern | mitigate | Route file contains ZERO raw SQL; all queries through `app/lib/repositories/monetization.repository.js`. `npm run route-sql:check` baseline held |
| T-03-03-06 | Information Disclosure | Counter SQL unmasks orgs outside the recency window | accept | 90-day recency window is by design — D-01 intent is "verified integrations in the wild", not historical. Window configurable via options; default is founder-friendly |
| T-03-03-07 | Information Disclosure | Livingcode auto-regen commits secrets | accept | Pre-existing generated paths (`app/lib/doctor/generated/*`, `public/livingcode/*`) never contain secrets per Phase 2 02-02 + 02-03 commit pattern; regen is deterministic from source. Not a new risk |
</threat_model>

<verification>
Plan-level verification (after Task 3 commit):

```bash
# All Wave-0 + new tests pass
npm test -- --run

# MON-01 public commitment — grep the 3 of 4 locations landed by this plan
grep -F "50 verified Claude Code integrations" .planning/PROJECT.md     # ≥1 match
grep -F "50 verified Claude Code integrations" README.md                # ≥1 match
# location 4 (/pricing) verified via pricing-page.test.jsx
# location 2 (launch tweet + HN) lands in Plan 03-02

# MON-02 tier gate works
npm test -- --run __tests__/unit/require-tier.test.js   # 5 cases PASS

# Repository pattern held
npm run route-sql:check             # baseline 85 or lower

# Static guardrails
npm run openapi:check
npm run api:inventory:check
npm run docs:check
node scripts/check-readme-lead.mjs  # exits 0

# Existing admin-gate non-regression (keys.route still 403s on non-admin)
npm test -- --run __tests__/unit/keys.route.test.js
```
</verification>

<success_criteria>
1. `requireTier('pro')` helper exported from `app/lib/org.js`; returns null for pro orgs, 403 COMING_SOON for free orgs (MON-02 D-07)
2. requireTier response body contains "Coming soon" + "50 verified" + "/pricing"; contains NO buy/upgrade/subscribe/pay language (D-07)
3. `/api/monetization/verified-integrations-count` public endpoint returns `{count, target: 50}`; response contains NO org_id or agent_id (T-03-03-02)
4. `app/lib/repositories/monetization.repository.js` encapsulates SQL (repository pattern per CLAUDE.md); route file contains zero raw SQL
5. `middleware.js` PUBLIC_ROUTES includes `/api/monetization/verified-integrations-count`
6. `/pricing` page exists at `app/pricing/page.js`; renders trigger commitment + live N/50 counter + Free tier list + Pro tier list + closing transparency paragraph (MON-01 D-01, D-03, D-05, D-06)
7. `.planning/PROJECT.md` contains the monetization trigger paragraph + Key Decisions row updated ✓ Locked (D-03 location 3)
8. `README.md` contains the monetization trigger paragraph AFTER the Claude-Code-first lead (D-03 location 4); `check-readme-lead.mjs` still passes (non-regression)
9. Full test suite `npm test -- --run` passes with all new Wave-0 scaffolds green
10. Static guardrails held: `route-sql:check`, `openapi:check`, `api:inventory:check`, `docs:check`, `check-readme-lead.mjs`
11. No schema migration required (organizations.plan column pre-exists at schema/schema.js:23; verified A11 fallback)
12. Paywall NOT shipped (per MON-02 D-07): `requireTier` is dormant helper; no routes in production use it except the test fixture
</success_criteria>

<output>
After completion, create `.planning/phases/03-public-launch/03-03-SUMMARY.md` documenting:
- requireTier helper shipped; 5 test cases covered
- Counter repository + API route shipped; 7+5 test cases covered; zero raw SQL in route
- /pricing page live with live N/50 counter; no paywall language
- MON-01 4-location commit: PROJECT.md + README.md + /pricing landed; launch tweet + HN body land in Plan 03-02
- D-07 flip-to-paid path: `UPDATE organizations SET plan='pro' WHERE id='org_<customer>'` — one SQL statement when MON-01 fires
- A8 resolution: agent_id ILIKE 'claude-code%' grounded in hooks/dashclaw_pretool.py:75 default
- Livingcode auto-regen artifacts committed alongside code (per CLAUDE.md §Generated Artifacts)
- Baseline route-SQL count held at 85 (or lower) per Pitfall 6
</output>
