thumbgate 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api/server.js CHANGED
@@ -219,6 +219,7 @@ const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
219
219
  const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
220
220
  const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
221
221
  const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
222
+ const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
222
223
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
223
224
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
224
225
  const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
@@ -413,26 +414,20 @@ const TRACKED_LINK_TARGETS = Object.freeze({
413
414
  },
414
415
  allowCustomerEmail: true,
415
416
  },
416
- // 2026-05-12: Aiventyx marketplace listing routes its Teams clicks through
417
- // /go/teams (best-performing listing at ~62% CTR). Without this slug the
418
- // server returned 404 + "Tracked link not found". Every Aiventyx Teams
419
- // click between the URL swap and this deploy landed on that error page.
420
- // Destination: 3-seat Team self-serve Stripe checkout (the path I shipped
421
- // in PR #1877 — plan_id=team + seat_count=3 = $147/mo entry).
417
+ // 2026-05-19: Team is intake-led. Keep the tracked shortlink alive for
418
+ // marketplaces and old outreach, but route it to workflow scope first
419
+ // instead of blind 3-seat checkout.
422
420
  teams: {
423
- path: '/checkout/pro',
421
+ path: '/#workflow-sprint-intake',
424
422
  ctaId: 'go_teams',
425
423
  ctaPlacement: 'link_router',
426
- eventType: 'cta_click',
424
+ eventType: 'team_intake_started',
427
425
  defaults: {
428
426
  utm_source: 'website',
429
427
  utm_medium: 'link_router',
430
- utm_campaign: 'team_self_serve',
428
+ utm_campaign: 'team_intake',
431
429
  plan_id: 'team',
432
- seat_count: '3',
433
- billing_cycle: 'monthly',
434
430
  },
435
- allowCustomerEmail: true,
436
431
  },
437
432
  install: {
438
433
  path: '/guide',
@@ -1384,7 +1379,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
1384
1379
  type: PROBLEM_TYPES.INVALID_REQUEST,
1385
1380
  title,
1386
1381
  status: 400,
1387
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
1382
+ detail: err?.message ? err.message : 'Invalid analytics window request.',
1388
1383
  });
1389
1384
  }
1390
1385
 
@@ -1499,50 +1494,11 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
1499
1494
 
1500
1495
  function renderCheckoutIntentPage({
1501
1496
  confirmHref,
1502
- firstRuleCheckoutHref,
1503
- quickReadCheckoutHref,
1504
- workflowTeardownCheckoutHref,
1505
1497
  workflowIntakeHref,
1506
- teamOptionsHref,
1507
- diagnosticCheckoutHref,
1508
- sprintCheckoutHref,
1509
- sprintDiagnosticPriceDollars = 499,
1510
- workflowSprintPriceDollars = 1500,
1511
1498
  }) {
1512
1499
  const safeConfirmHref = escapeHtmlAttribute(confirmHref);
1513
- const safeFirstRuleCheckoutHref = firstRuleCheckoutHref
1514
- ? escapeHtmlAttribute(firstRuleCheckoutHref)
1515
- : '';
1516
- const safeQuickReadCheckoutHref = quickReadCheckoutHref
1517
- ? escapeHtmlAttribute(quickReadCheckoutHref)
1518
- : '';
1519
- const safeWorkflowTeardownCheckoutHref = workflowTeardownCheckoutHref
1520
- ? escapeHtmlAttribute(workflowTeardownCheckoutHref)
1521
- : '';
1522
1500
  const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
1523
- const safeTeamOptionsHref = escapeHtmlAttribute(teamOptionsHref);
1524
- const safeDiagnosticCheckoutHref = diagnosticCheckoutHref
1525
- ? escapeHtmlAttribute(diagnosticCheckoutHref)
1526
- : '';
1527
- const safeSprintCheckoutHref = sprintCheckoutHref
1528
- ? escapeHtmlAttribute(sprintCheckoutHref)
1529
- : '';
1530
- const diagnosticAction = safeDiagnosticCheckoutHref
1531
- ? `<a data-i="sprint_diagnostic_checkout" href="${safeDiagnosticCheckoutHref}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
1532
- : '';
1533
- const sprintAction = safeSprintCheckoutHref
1534
- ? `<a data-i="workflow_sprint_checkout" href="${safeSprintCheckoutHref}">Start $${workflowSprintPriceDollars} sprint</a>`
1535
- : '';
1536
- const firstRuleAction = safeFirstRuleCheckoutHref
1537
- ? `<a data-i="first_failure_rule_checkout" href="${safeFirstRuleCheckoutHref}">Pay $1 first rule</a>`
1538
- : '';
1539
- const quickReadAction = safeQuickReadCheckoutHref
1540
- ? `<a data-i="quick_read_checkout" href="${safeQuickReadCheckoutHref}">Pay $19 quick read</a>`
1541
- : '';
1542
- const teardownAction = safeWorkflowTeardownCheckoutHref
1543
- ? `<a data-i="workflow_teardown_checkout" href="${safeWorkflowTeardownCheckoutHref}">Pay $99 teardown</a>`
1544
- : '';
1545
- return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm — ThumbGate Pro</title><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}main{max-width:520px;margin:8vh auto;padding:0 20px}.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}.brand-mark{width:24px;height:24px;background:#22d3ee;border-radius:6px;display:inline-block}h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}p{color:#cbd5e1;margin:8px 0}a{display:block;text-decoration:none}a.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:20px 0 10px}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0;font-size:14px}.trust{margin:24px 0;padding:16px;border:1px solid #1f2937;border-radius:8px;background:#0f172a}.trust-item{font-size:13px;color:#cbd5e1;padding:4px 0;display:flex;gap:8px}.trust-item::before{content:"✓";color:#22d3ee;font-weight:700}details{margin-top:32px;font-size:13px;color:#94a3b8}details summary{cursor:pointer;padding:8px 0}details a{border:1px solid #374151;color:#94a3b8;padding:10px;text-align:center;border-radius:6px;margin:6px 0;font-size:13px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}</style><main><div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div><h1>Start ThumbGate Pro</h1><div class="price">$19<small>/mo</small></div><p>Block every repeat AI-agent mistake. Local-first. MIT-licensed CLI included. Cancel anytime.</p><a class="primary" data-i="pro_checkout_confirmed" href="${safeConfirmHref}">Pay $19/mo with Stripe →</a><div class="trust"><div class="trust-item">6 paying customers, 18,000+ installs verified on npm</div><div class="trust-item">Cancel anytime — instant refund within 7 days</div><div class="trust-item">MIT open source · no vendor lock-in</div><div class="trust-item">Works with Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode</div></div><details><summary>Other paid paths (diagnostic, sprint, teardown, single-rule)</summary>${diagnosticAction.replace('<a ', '<a class="secondary" ')}${sprintAction.replace('<a ', '<a class="secondary" ')}${teardownAction.replace('<a ', '<a class="secondary" ')}${quickReadAction.replace('<a ', '<a class="secondary" ')}${firstRuleAction.replace('<a ', '<a class="secondary" ')}<a class="secondary" data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Send workflow first (intake)</a><a class="secondary" data-i="team_paid_path" href="${safeTeamOptionsHref}">See all options</a></details><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>addEventListener('click',e=>{let a=e.target.closest('[data-i]');if(a&&navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:a.dataset.i,ctaPlacement:'checkout_interstitial'})],{type:'application/json'}))})</script></html>`;
1501
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm — ThumbGate Pro</title><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}main{max-width:520px;margin:8vh auto;padding:0 20px}.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}.brand-mark{width:24px;height:24px;background:#22d3ee;border-radius:6px;display:inline-block}h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}p{color:#cbd5e1;margin:8px 0}a{display:block;text-decoration:none}a.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:20px 0 10px}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0 0;font-size:14px}.trust{margin:24px 0;padding:16px;border:1px solid #1f2937;border-radius:8px;background:#0f172a}.trust-item{font-size:13px;color:#cbd5e1;padding:4px 0;display:flex;gap:8px}.trust-item::before{content:"✓";color:#22d3ee;font-weight:700}.choice-note{font-size:13px;color:#94a3b8;margin-top:14px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}</style><main><div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div><h1>Start ThumbGate Pro</h1><div class="price">$19<small>/mo</small></div><p>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p><a class="primary" data-i="pro_checkout_confirmed" rel="nofollow noindex" href="${safeConfirmHref}">Pay $19/mo with Stripe →</a><a class="secondary" data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Not sure yet? Send the workflow first</a><p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p><div class="trust"><div class="trust-item">Lessons synced across all your machines — no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine — SonarCloud regressions fixed in &lt;24h</div></div><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>addEventListener('click',e=>{let a=e.target.closest('[data-i]');if(a&&navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:a.dataset.i,ctaPlacement:'checkout_interstitial'})],{type:'application/json'}))})</script></html>`;
1546
1502
  }
1547
1503
 
1548
1504
  function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
@@ -1851,7 +1807,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
1851
1807
  metadata: {
1852
1808
  context,
1853
1809
  eventType: payload && (payload.eventType || payload.event) ? payload.eventType || payload.event : 'unknown',
1854
- error: err && err.message ? err.message : 'unknown_error',
1810
+ error: err?.message ? err.message : 'unknown_error',
1855
1811
  },
1856
1812
  diagnosis: {
1857
1813
  diagnosed: true,
@@ -1861,7 +1817,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
1861
1817
  constraintId: 'telemetry:emit',
1862
1818
  message: 'Server-side telemetry write failed.',
1863
1819
  }],
1864
- evidence: [err && err.message ? err.message : 'unknown_error'],
1820
+ evidence: [err?.message ? err.message : 'unknown_error'],
1865
1821
  },
1866
1822
  });
1867
1823
  } catch (_) {}
@@ -2007,6 +1963,10 @@ function loadProPageHtml(runtimeConfig, pageContext = {}) {
2007
1963
  return loadPublicMarketingTemplateHtml(PRO_PAGE_PATH, runtimeConfig, pageContext);
2008
1964
  }
2009
1965
 
1966
+ function loadPricingPageHtml(runtimeConfig, pageContext = {}) {
1967
+ return loadPublicMarketingTemplateHtml(PRICING_PAGE_PATH, runtimeConfig, pageContext);
1968
+ }
1969
+
2010
1970
  function readOptionalPublicTemplate(filePath) {
2011
1971
  try {
2012
1972
  return fs.readFileSync(filePath, 'utf-8');
@@ -2541,6 +2501,7 @@ function renderSitemapXml(runtimeConfig) {
2541
2501
  const entries = [
2542
2502
  { path: '/', changefreq: 'weekly', priority: '1.0' },
2543
2503
  { path: '/pro', changefreq: 'weekly', priority: '0.9' },
2504
+ { path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
2544
2505
  { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
2545
2506
  { path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
2546
2507
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
@@ -3019,13 +2980,13 @@ function renderCheckoutSuccessPage(runtimeConfig) {
3019
2980
  curlBlock.textContent = body.nextSteps && body.nextSteps.curl ? body.nextSteps.curl : 'curl snippet unavailable.';
3020
2981
  } catch (err) {
3021
2982
  sendTelemetryOnce('checkout_session_lookup_failed', {
3022
- failureCode: err && err.message ? err.message : 'checkout_session_lookup_failed',
2983
+ failureCode: err?.message ? err.message : 'checkout_session_lookup_failed',
3023
2984
  });
3024
2985
  statusEl.textContent = 'Provisioning lookup failed.';
3025
2986
  summaryEl.textContent = traceId
3026
2987
  ? 'You can retry this page. If it keeps failing, inspect the hosted API logs with trace ' + traceId + '.'
3027
2988
  : 'You can retry this page. If it keeps failing, inspect the hosted API logs.';
3028
- keyBlock.textContent = err && err.message ? err.message : 'Unknown error';
2989
+ keyBlock.textContent = err?.message ? err.message : 'Unknown error';
3029
2990
  }
3030
2991
  }
3031
2992
 
@@ -3391,6 +3352,39 @@ function renderWorkflowSprintIntakeResultPage(runtimeConfig, { title, detail, le
3391
3352
  </html>`;
3392
3353
  }
3393
3354
 
3355
+ function renderBrokerAuditIntakeResultPage(runtimeConfig, { title, detail, leadId = null }) {
3356
+ const safeTitle = escapeHtmlAttribute(title || 'Broker audit request received');
3357
+ const safeDetail = escapeHtmlAttribute(detail || 'Your broker lead-flow audit request is in the queue.');
3358
+ const safeLeadId = leadId ? `<p><strong>Lead ID:</strong> ${escapeHtmlAttribute(leadId)}</p>` : '';
3359
+ return `<!DOCTYPE html>
3360
+ <html lang="en">
3361
+ <head>
3362
+ <meta charset="UTF-8" />
3363
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3364
+ <title>${safeTitle}</title>
3365
+ <meta name="robots" content="noindex,nofollow" />
3366
+ <style>
3367
+ body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: #0b1220; color: #e5edf8; line-height: 1.6; }
3368
+ main { max-width: 680px; margin: 0 auto; padding: 72px 20px; }
3369
+ .card { border: 1px solid #26354f; border-radius: 14px; padding: 24px; background: #111a2b; }
3370
+ h1 { margin: 0 0 10px; font-size: 30px; }
3371
+ p { color: #b8c5d8; }
3372
+ a { color: #7dd3fc; font-weight: 700; text-decoration: none; }
3373
+ </style>
3374
+ </head>
3375
+ <body>
3376
+ <main>
3377
+ <div class="card">
3378
+ <h1>${safeTitle}</h1>
3379
+ <p>${safeDetail}</p>
3380
+ ${safeLeadId}
3381
+ <p><a href="${escapeHtmlAttribute(runtimeConfig.appOrigin)}/broker-audit">Back to broker audit page</a></p>
3382
+ </div>
3383
+ </main>
3384
+ </body>
3385
+ </html>`;
3386
+ }
3387
+
3394
3388
  function readBodyBuffer(req, maxBytes = 1024 * 1024) {
3395
3389
  return new Promise((resolve, reject) => {
3396
3390
  let total = 0;
@@ -3435,6 +3429,72 @@ async function parseFormBody(req, maxBytes = 1024 * 1024) {
3435
3429
  return Object.fromEntries(params.entries());
3436
3430
  }
3437
3431
 
3432
+ function normalizeLeadEmail(value) {
3433
+ const email = normalizeNullableText(value);
3434
+ if (!email || !/^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{1,63}$/.test(email)) {
3435
+ throw createHttpError(400, 'A valid email is required');
3436
+ }
3437
+ return email.toLowerCase();
3438
+ }
3439
+
3440
+ function normalizeHttpUrl(value, fieldName) {
3441
+ const raw = normalizeNullableText(value);
3442
+ if (!raw) throw createHttpError(400, `${fieldName} is required`);
3443
+ try {
3444
+ const parsedUrl = new URL(raw);
3445
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
3446
+ throw new Error('unsupported protocol');
3447
+ }
3448
+ return parsedUrl.toString();
3449
+ } catch {
3450
+ throw createHttpError(400, `${fieldName} must be an http(s) URL`);
3451
+ }
3452
+ }
3453
+
3454
+ function appendBrokerAuditLead(feedbackDir, payload) {
3455
+ const lead = {
3456
+ leadId: createTraceId('broker_audit_lead'),
3457
+ status: 'new',
3458
+ offer: 'broker_lead_flow_audit_free',
3459
+ requestedAt: new Date().toISOString(),
3460
+ contact: {
3461
+ name: normalizeNullableText(payload.name),
3462
+ email: normalizeLeadEmail(payload.email),
3463
+ brokerage: normalizeNullableText(payload.brokerage),
3464
+ website: normalizeHttpUrl(payload.website, 'website'),
3465
+ },
3466
+ qualification: {
3467
+ suspectedLeak: normalizeNullableText(payload.suspected_leak || payload.suspectedLeak),
3468
+ },
3469
+ attribution: {
3470
+ traceId: payload.traceId,
3471
+ acquisitionId: payload.acquisitionId,
3472
+ visitorId: payload.visitorId,
3473
+ sessionId: payload.sessionId,
3474
+ source: payload.source,
3475
+ utmSource: payload.utmSource,
3476
+ utmMedium: payload.utmMedium,
3477
+ utmCampaign: payload.utmCampaign,
3478
+ utmContent: payload.utmContent,
3479
+ utmTerm: payload.utmTerm,
3480
+ ctaId: payload.ctaId,
3481
+ ctaPlacement: payload.ctaPlacement,
3482
+ page: payload.page,
3483
+ landingPath: payload.landingPath,
3484
+ referrer: payload.referrer,
3485
+ referrerHost: payload.referrerHost,
3486
+ },
3487
+ };
3488
+
3489
+ if (!lead.contact.name) throw createHttpError(400, 'name is required');
3490
+ if (!lead.contact.brokerage) throw createHttpError(400, 'brokerage is required');
3491
+
3492
+ const leadsPath = path.join(feedbackDir, 'broker-audit-leads.jsonl');
3493
+ fs.mkdirSync(path.dirname(leadsPath), { recursive: true });
3494
+ fs.appendFileSync(leadsPath, `${JSON.stringify(lead)}\n`, 'utf8');
3495
+ return lead;
3496
+ }
3497
+
3438
3498
  function parseOptionalObject(input, name) {
3439
3499
  if (input == null) return {};
3440
3500
  if (typeof input === 'object' && !Array.isArray(input)) return input;
@@ -3894,7 +3954,7 @@ function createApiServer() {
3894
3954
  }
3895
3955
  })
3896
3956
  .catch((err) => {
3897
- console.warn('[newsletter] welcome email threw:', email, err && err.message);
3957
+ console.warn('[newsletter] welcome email threw:', email, err?.message);
3898
3958
  });
3899
3959
  }
3900
3960
  const journeyState = resolveJourneyState(req, parsed);
@@ -4372,6 +4432,29 @@ async function addContext(){
4372
4432
  return;
4373
4433
  }
4374
4434
 
4435
+ // Natural marketing URLs for the Workflow Hardening Sprint. Outbound
4436
+ // messages, social posts, and word-of-mouth all refer to "the sprint"
4437
+ // or "workflow hardening" — recipients who type or paste the natural
4438
+ // URLs currently hit the generic API 401 JSON error and bounce.
4439
+ // Redirect them to the canonical intake anchor instead.
4440
+ if (isGetLikeRequest && (
4441
+ pathname === '/sprint'
4442
+ || pathname === '/sprint.html'
4443
+ || pathname === '/workflow-hardening'
4444
+ || pathname === '/workflow-hardening.html'
4445
+ || pathname === '/workflow-hardening-sprint'
4446
+ || pathname === '/workflow-hardening-sprint.html'
4447
+ || pathname === '/workflow-sprint'
4448
+ || pathname === '/workflow-sprint.html'
4449
+ )) {
4450
+ res.writeHead(302, {
4451
+ Location: '/#workflow-sprint-intake',
4452
+ 'Cache-Control': 'no-store',
4453
+ });
4454
+ res.end();
4455
+ return;
4456
+ }
4457
+
4375
4458
  if (isGetLikeRequest && (pathname === '/numbers' || pathname === '/numbers.html')) {
4376
4459
  // Route through servePublicMarketingPage so landing_page_view telemetry
4377
4460
  // + funnel-events.jsonl `discovery/landing_view` get captured with UTM
@@ -4414,6 +4497,29 @@ async function addContext(){
4414
4497
  return;
4415
4498
  }
4416
4499
 
4500
+ if (isGetLikeRequest && (pathname === '/agent-manager' || pathname === '/agent-manager.html')) {
4501
+ // ICP landing page for the role Anthropic named (Agent Manager —
4502
+ // hybrid PM/engineer DRI who owns CLAUDE.md hierarchy, plugin
4503
+ // marketplace, permissions policy, and which skills ship). Routed
4504
+ // through servePublicMarketingPage so arrivals via X/Bluesky/LinkedIn
4505
+ // threads about the role capture UTM attribution and
4506
+ // landing_page_view telemetry for downstream pilot-pipeline analysis.
4507
+ try {
4508
+ servePublicMarketingPage({
4509
+ req,
4510
+ res,
4511
+ parsed,
4512
+ hostedConfig,
4513
+ isHeadRequest,
4514
+ renderHtml: () => fs.readFileSync(path.join(PUBLIC_DIR, 'agent-manager.html'), 'utf-8'),
4515
+ extraTelemetry: { pageType: 'agent_manager' },
4516
+ });
4517
+ } catch {
4518
+ sendJson(res, 404, { error: 'Agent Manager page not found' });
4519
+ }
4520
+ return;
4521
+ }
4522
+
4417
4523
  if (isGetLikeRequest && pathname === '/learn/learn.css') {
4418
4524
  try {
4419
4525
  const cssPath = path.join(LEARN_DIR, 'learn.css');
@@ -4505,7 +4611,7 @@ async function addContext(){
4505
4611
  version: pkg.version,
4506
4612
  status: 'ok',
4507
4613
  docs: 'https://github.com/IgorGanapolsky/ThumbGate',
4508
- endpoints: ['/health', '/dashboard', '/guide', '/codex-plugin', '/compare', '/learn', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
4614
+ endpoints: ['/health', '/dashboard', '/guide', '/codex-plugin', '/compare', '/learn', '/pricing', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
4509
4615
  }, {}, {
4510
4616
  headOnly: isHeadRequest,
4511
4617
  });
@@ -4549,9 +4655,22 @@ async function addContext(){
4549
4655
 
4550
4656
  const botClassification = classifyRequester(req.headers);
4551
4657
  const confirmParam = parsed?.searchParams?.get('confirm') ?? null;
4552
- const isConfirmedCheckout = confirmParam === '1'
4553
- || confirmParam === 'true'
4554
- || req.method === 'POST';
4658
+ const hasConfirmFlag = confirmParam === '1' || confirmParam === 'true';
4659
+ // 2026-05-19 audit: 2,210 of 2,251 Stripe sessions ever created were
4660
+ // zombies (expired with no email). Cause: the interstitial HTML
4661
+ // rendered a `/checkout/pro?confirm=1` link, bot crawlers discovered
4662
+ // it and followed it, bypassing the bot deflection. A bot crawl never
4663
+ // reaches the payment form, so the session is guaranteed waste.
4664
+ //
4665
+ // Fix: bot + confirm=1 (alone) deflects back to the interstitial. Two
4666
+ // escape hatches preserve real-user flows: (a) POST always proceeds
4667
+ // (form submission JS-less bots don't do), (b) a `customer_email`
4668
+ // query param treats the request as confirmed even from a bot UA,
4669
+ // because no real crawler appends customer_email to discovered URLs.
4670
+ const hasCustomerEmailHint = !!parsed?.searchParams?.has('customer_email');
4671
+ const botShouldBypass = !botClassification.isBot || hasCustomerEmailHint;
4672
+ const isConfirmedCheckout = req.method === 'POST'
4673
+ || (hasConfirmFlag && botShouldBypass);
4555
4674
  // Plausible funnel event #1 of 3: page view. Fired before interstitial
4556
4675
  // deflection so we get the full top-of-funnel count, with isBot as a
4557
4676
  // prop so the dashboard can filter human vs. crawler traffic. Fire-and-forget.
@@ -4582,7 +4701,7 @@ async function addContext(){
4582
4701
  // and must click "Pay $19/mo with Stripe →" (which sets confirm=1) to
4583
4702
  // trigger the Stripe-session creation + redirect. Counter-risk: one
4584
4703
  // extra click on the human path. Mitigated because the interstitial
4585
- // also serves as a value-preview ("6 paying customers, MIT open source,
4704
+ // also serves as a value-preview ("5,200+ npm installs/mo, MIT open source,
4586
4705
  // cancel anytime"), which typically lifts conversion more than the
4587
4706
  // click-friction costs.
4588
4707
  if (!isConfirmedCheckout) {
@@ -4617,67 +4736,9 @@ async function addContext(){
4617
4736
  ctaPlacement: 'checkout_interstitial',
4618
4737
  planId: 'team',
4619
4738
  });
4620
- const teamOptionsHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/guides/ai-agent-governance-sprint`, analyticsMetadata, {
4621
- utmMedium: 'checkout_interstitial_paid_path',
4622
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_team_paid_path',
4623
- ctaId: 'checkout_interstitial_team_paid_path',
4624
- ctaPlacement: 'checkout_interstitial',
4625
- planId: 'team',
4626
- });
4627
- const firstRuleCheckoutHref = buildCheckoutIntentHref(FIRST_FAILURE_RULE_CHECKOUT_URL, analyticsMetadata, {
4628
- utmMedium: 'checkout_interstitial_paid_path',
4629
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_first_failure_rule',
4630
- ctaId: 'checkout_interstitial_first_failure_rule_checkout',
4631
- ctaPlacement: 'checkout_interstitial',
4632
- planId: 'first_failure_rule',
4633
- });
4634
- const quickReadCheckoutHref = buildCheckoutIntentHref(QUICK_READ_CHECKOUT_URL, analyticsMetadata, {
4635
- utmMedium: 'checkout_interstitial_paid_path',
4636
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_quick_read',
4637
- ctaId: 'checkout_interstitial_quick_read_checkout',
4638
- ctaPlacement: 'checkout_interstitial',
4639
- planId: 'quick_read',
4640
- });
4641
- const workflowTeardownCheckoutHref = buildCheckoutIntentHref(WORKFLOW_TEARDOWN_CHECKOUT_URL, analyticsMetadata, {
4642
- utmMedium: 'checkout_interstitial_paid_path',
4643
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_teardown',
4644
- ctaId: 'checkout_interstitial_workflow_teardown_checkout',
4645
- ctaPlacement: 'checkout_interstitial',
4646
- planId: 'workflow_teardown',
4647
- });
4648
- const diagnosticCheckoutHref = buildCheckoutIntentHref(
4649
- hostedConfig.sprintDiagnosticCheckoutUrl || SPRINT_DIAGNOSTIC_CHECKOUT_URL,
4650
- analyticsMetadata,
4651
- {
4652
- utmMedium: 'checkout_interstitial_paid_path',
4653
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_diagnostic',
4654
- ctaId: 'checkout_interstitial_sprint_diagnostic_checkout',
4655
- ctaPlacement: 'checkout_interstitial',
4656
- planId: 'sprint_diagnostic',
4657
- }
4658
- );
4659
- const sprintCheckoutHref = buildCheckoutIntentHref(
4660
- hostedConfig.workflowSprintCheckoutUrl || WORKFLOW_SPRINT_CHECKOUT_URL,
4661
- analyticsMetadata,
4662
- {
4663
- utmMedium: 'checkout_interstitial_paid_path',
4664
- utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4665
- ctaId: 'checkout_interstitial_workflow_sprint_checkout',
4666
- ctaPlacement: 'checkout_interstitial',
4667
- planId: 'workflow_sprint',
4668
- }
4669
- );
4670
4739
  const html = renderCheckoutIntentPage({
4671
4740
  confirmHref: buildCheckoutConfirmHref(parsed),
4672
- firstRuleCheckoutHref,
4673
- quickReadCheckoutHref,
4674
- workflowTeardownCheckoutHref,
4675
4741
  workflowIntakeHref,
4676
- teamOptionsHref,
4677
- diagnosticCheckoutHref,
4678
- sprintCheckoutHref,
4679
- sprintDiagnosticPriceDollars: hostedConfig.sprintDiagnosticPriceDollars || 499,
4680
- workflowSprintPriceDollars: hostedConfig.workflowSprintPriceDollars || 1500,
4681
4742
  botClassification,
4682
4743
  });
4683
4744
  sendHtml(res, 200, html, responseHeaders);
@@ -4880,7 +4941,7 @@ async function addContext(){
4880
4941
  seatCount: analyticsMetadata.seatCount,
4881
4942
  referrer: analyticsMetadata.referrer,
4882
4943
  referrerHost: analyticsMetadata.referrerHost,
4883
- failureCode: err && err.message ? err.message : 'checkout_bootstrap_failed',
4944
+ failureCode: err?.message ? err.message : 'checkout_bootstrap_failed',
4884
4945
  httpStatus: err && err.statusCode ? err.statusCode : null,
4885
4946
  }, req.headers, 'checkout_api_failed');
4886
4947
  res.writeHead(302, {
@@ -4938,6 +4999,28 @@ async function addContext(){
4938
4999
  return;
4939
5000
  }
4940
5001
 
5002
+ if (isGetLikeRequest && pathname === '/broker-audit') {
5003
+ // Public-facing broker lead-flow audit landing page. Wedge for the
5004
+ // real-estate broker outreach. Static HTML served from src/api/static.
5005
+ try {
5006
+ const html = normalizePublicMarketingHtml(fillTemplate(fs.readFileSync(
5007
+ path.resolve(__dirname, '../../assets/static/broker-audit.html'),
5008
+ 'utf8'
5009
+ ), {
5010
+ '__POSTHOG_API_KEY__': hostedConfig.posthogApiKey || '',
5011
+ }), hostedConfig);
5012
+ if (isHeadRequest) {
5013
+ sendHtml(res, 200, html, {}, { headOnly: true });
5014
+ return;
5015
+ }
5016
+ sendHtml(res, 200, html);
5017
+ } catch (err) {
5018
+ console.error('broker-audit page read failed:', err?.message);
5019
+ sendJson(res, 500, { error: 'broker-audit page unavailable' });
5020
+ }
5021
+ return;
5022
+ }
5023
+
4941
5024
  if (isGetLikeRequest && pathname === '/.well-known/mcp.json') {
4942
5025
  sendJson(res, 200, getMcpDiscoveryManifest(hostedConfig), {}, {
4943
5026
  headOnly: isHeadRequest,
@@ -5147,7 +5230,7 @@ async function addContext(){
5147
5230
  path: pathname,
5148
5231
  method: req.method,
5149
5232
  reason: err && err.statusCode && err.statusCode < 500 ? 'invalid_payload' : 'write_failed',
5150
- error: err && err.message ? err.message : 'unknown_error',
5233
+ error: err?.message ? err.message : 'unknown_error',
5151
5234
  },
5152
5235
  diagnosis: {
5153
5236
  diagnosed: true,
@@ -5157,7 +5240,7 @@ async function addContext(){
5157
5240
  constraintId: 'telemetry:ingest',
5158
5241
  message: 'Telemetry ping could not be processed.',
5159
5242
  }],
5160
- evidence: [err && err.message ? err.message : 'unknown_error'],
5243
+ evidence: [err?.message ? err.message : 'unknown_error'],
5161
5244
  },
5162
5245
  });
5163
5246
  } catch (_) {
@@ -5356,11 +5439,270 @@ async function addContext(){
5356
5439
  return;
5357
5440
  }
5358
5441
 
5442
+ if (req.method === 'OPTIONS' && pathname === '/v1/marketing/install-email') {
5443
+ // CORS preflight for the npm postinstall email-capture endpoint.
5444
+ // The endpoint is called from `npx thumbgate subscribe <email>` so
5445
+ // the CLI gets the right CORS headers if it ever hits the proxy
5446
+ // path; in practice CLI hits the server directly so this is mostly
5447
+ // defense-in-depth.
5448
+ sendPublicBillingPreflight(res);
5449
+ return;
5450
+ }
5451
+
5452
+ if (req.method === 'POST' && pathname === '/v1/marketing/install-email') {
5453
+ // Email-capture wedge for npm installers. The `subscribe` CLI
5454
+ // subcommand POSTs { email, source, installId, cliVersion } here;
5455
+ // we validate the email, persist it to a dedicated capture ledger
5456
+ // (telemetry sanitizer would strip the email as PII), emit a
5457
+ // privacy-clean telemetry ping for funnel attribution, and fire
5458
+ // a Resend newsletter welcome (if RESEND_API_KEY is configured).
5459
+ // No auth required — this is a public opt-in surface.
5460
+ const { FEEDBACK_DIR } = getFeedbackPaths();
5461
+ let body = '';
5462
+ let total = 0;
5463
+ let oversize = false;
5464
+ const MAX_BODY = 2048;
5465
+ req.on('data', (chunk) => {
5466
+ total += chunk.length;
5467
+ if (total > MAX_BODY) {
5468
+ oversize = true;
5469
+ return; // stop appending; let 'end' fire naturally to send 413
5470
+ }
5471
+ body += chunk;
5472
+ });
5473
+ req.on('end', async () => {
5474
+ if (oversize) {
5475
+ sendJson(res, 413, { error: 'payload_too_large' });
5476
+ return;
5477
+ }
5478
+ let parsed;
5479
+ try {
5480
+ parsed = body ? JSON.parse(body) : {};
5481
+ } catch {
5482
+ sendJson(res, 400, { error: 'invalid_json' });
5483
+ return;
5484
+ }
5485
+ const rawEmail = typeof parsed.email === 'string' ? parsed.email.trim() : '';
5486
+ // RFC 5321-bounded email shape — same regex shape as the CLI side
5487
+ // to keep the contract honest.
5488
+ const emailValid = /^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{1,64}$/.test(rawEmail);
5489
+ if (!emailValid) {
5490
+ sendJson(res, 400, { error: 'invalid_email' });
5491
+ return;
5492
+ }
5493
+ const source = typeof parsed.source === 'string' && parsed.source.length <= 64
5494
+ ? parsed.source
5495
+ : 'cli_subscribe';
5496
+ const installId = typeof parsed.installId === 'string' && parsed.installId.length <= 128
5497
+ ? parsed.installId
5498
+ : null;
5499
+ const cliVersion = typeof parsed.cliVersion === 'string' && parsed.cliVersion.length <= 32
5500
+ ? parsed.cliVersion
5501
+ : null;
5502
+
5503
+ // Persist the capture to a dedicated ledger. The standard
5504
+ // telemetry sanitizer in scripts/telemetry-analytics.js
5505
+ // intentionally strips arbitrary fields (PII protection), so
5506
+ // we cannot rely on appendBestEffortTelemetry to preserve the
5507
+ // email. The capture ledger lives alongside other feedback
5508
+ // artifacts and is the source of truth for the marketing drip.
5509
+ try {
5510
+ const fsModule = require('node:fs');
5511
+ const pathModule = require('node:path');
5512
+ const captureDir = pathModule.resolve(FEEDBACK_DIR);
5513
+ fsModule.mkdirSync(captureDir, { recursive: true });
5514
+ const capturePath = pathModule.join(captureDir, 'marketing-install-emails.jsonl');
5515
+ fsModule.appendFileSync(capturePath, JSON.stringify({
5516
+ capturedAt: new Date().toISOString(),
5517
+ email: rawEmail,
5518
+ source,
5519
+ installId,
5520
+ cliVersion,
5521
+ remoteAddr: req.socket?.remoteAddress || null,
5522
+ userAgent: req.headers['user-agent'] || null,
5523
+ }) + '\n', 'utf-8');
5524
+ } catch (err) {
5525
+ // Capture failure is recoverable — we still want to fire the
5526
+ // welcome email and surface the failure to ops via diagnostic.
5527
+ try {
5528
+ const { appendDiagnosticRecord } = require('../../scripts/feedback-loop');
5529
+ appendDiagnosticRecord({
5530
+ source: 'install_email_capture',
5531
+ step: 'capture_persist',
5532
+ context: 'failed to persist install-email capture to ledger',
5533
+ metadata: { error: err?.message || 'unknown' },
5534
+ });
5535
+ } catch (_) {}
5536
+ }
5537
+
5538
+ // Privacy-clean telemetry ping for funnel attribution (no email).
5539
+ appendBestEffortTelemetry(FEEDBACK_DIR, {
5540
+ eventType: 'marketing_install_email_captured',
5541
+ clientType: 'cli',
5542
+ source,
5543
+ installId,
5544
+ cliVersion,
5545
+ utmSource: source,
5546
+ utmMedium: 'npm_postinstall',
5547
+ utmCampaign: 'install_email_capture',
5548
+ }, req.headers, 'marketing_install_email_captured');
5549
+
5550
+ // Fire Resend welcome. If RESEND_API_KEY is unset the mailer
5551
+ // returns { sent: false, reason: 'no_api_key' } and the
5552
+ // capture still succeeds — the operator can drip later from
5553
+ // the captured ledger.
5554
+ let mailerResult = { sent: false, reason: 'not_attempted' };
5555
+ try {
5556
+ const { sendNewsletterWelcomeEmail } = require('../../scripts/mailer');
5557
+ mailerResult = await sendNewsletterWelcomeEmail({
5558
+ to: rawEmail,
5559
+ source,
5560
+ installId,
5561
+ cliVersion,
5562
+ });
5563
+ } catch (err) {
5564
+ mailerResult = { sent: false, reason: `mailer_error:${err.message || 'unknown'}` };
5565
+ }
5566
+
5567
+ sendJson(res, 200, {
5568
+ ok: true,
5569
+ captured: true,
5570
+ mailerSent: !!mailerResult.sent,
5571
+ mailerReason: mailerResult.reason || null,
5572
+ });
5573
+ });
5574
+ return;
5575
+ }
5576
+
5359
5577
  if (req.method === 'OPTIONS' && pathname === '/v1/intake/workflow-sprint') {
5360
5578
  sendPublicBillingPreflight(res);
5361
5579
  return;
5362
5580
  }
5363
5581
 
5582
+ if (req.method === 'OPTIONS' && pathname === '/v1/intake/broker-audit') {
5583
+ sendPublicBillingPreflight(res);
5584
+ return;
5585
+ }
5586
+
5587
+ if (req.method === 'POST' && pathname === '/v1/intake/broker-audit') {
5588
+ const { FEEDBACK_DIR } = getFeedbackPaths();
5589
+ const traceId = createTraceId('broker_audit');
5590
+ const journeyState = resolveJourneyState(req, parsed);
5591
+ const referrerAttribution = buildReferrerAttribution(req);
5592
+ const contentType = String(req.headers['content-type'] || '').toLowerCase();
5593
+ const isFormSubmission = contentType.includes('application/x-www-form-urlencoded');
5594
+ try {
5595
+ const body = isFormSubmission
5596
+ ? await parseFormBody(req, 16 * 1024)
5597
+ : await parseJsonBody(req, 16 * 1024);
5598
+ const lead = appendBrokerAuditLead(FEEDBACK_DIR, {
5599
+ ...body,
5600
+ traceId: body.traceId || traceId,
5601
+ acquisitionId: body.acquisitionId || journeyState.acquisitionId,
5602
+ visitorId: body.visitorId || journeyState.visitorId,
5603
+ sessionId: body.sessionId || journeyState.sessionId,
5604
+ page: body.page || referrerAttribution.page || '/broker-audit',
5605
+ landingPath: body.landingPath || referrerAttribution.landingPath || '/broker-audit',
5606
+ ctaId: body.ctaId || 'broker_audit_form',
5607
+ ctaPlacement: body.ctaPlacement || 'broker_audit',
5608
+ source: body.source || body.utmSource || referrerAttribution.source || 'website',
5609
+ utmSource: body.utmSource || body.source || referrerAttribution.utmSource || 'website',
5610
+ utmMedium: body.utmMedium || referrerAttribution.utmMedium || 'broker_audit',
5611
+ utmCampaign: body.utmCampaign || referrerAttribution.utmCampaign || 'broker_audit_free',
5612
+ utmContent: body.utmContent || referrerAttribution.utmContent || null,
5613
+ utmTerm: body.utmTerm || referrerAttribution.utmTerm || null,
5614
+ referrerHost: body.referrerHost || referrerAttribution.referrerHost || null,
5615
+ referrer: body.referrer || referrerAttribution.referrer || null,
5616
+ });
5617
+
5618
+ appendBestEffortTelemetry(FEEDBACK_DIR, {
5619
+ eventType: 'broker_audit_lead_submitted',
5620
+ clientType: 'web',
5621
+ traceId: lead.attribution.traceId,
5622
+ acquisitionId: lead.attribution.acquisitionId,
5623
+ visitorId: lead.attribution.visitorId,
5624
+ sessionId: lead.attribution.sessionId,
5625
+ source: lead.attribution.source,
5626
+ utmSource: lead.attribution.utmSource,
5627
+ utmMedium: lead.attribution.utmMedium,
5628
+ utmCampaign: lead.attribution.utmCampaign,
5629
+ utmContent: lead.attribution.utmContent,
5630
+ utmTerm: lead.attribution.utmTerm,
5631
+ ctaId: lead.attribution.ctaId,
5632
+ ctaPlacement: lead.attribution.ctaPlacement,
5633
+ page: lead.attribution.page,
5634
+ landingPath: lead.attribution.landingPath,
5635
+ referrerHost: lead.attribution.referrerHost,
5636
+ referrer: lead.attribution.referrer,
5637
+ }, req.headers, 'broker_audit_lead_submitted');
5638
+
5639
+ if (isFormSubmission && !wantsJson(req, parsed)) {
5640
+ sendHtml(
5641
+ res,
5642
+ 201,
5643
+ renderBrokerAuditIntakeResultPage(hostedConfig, {
5644
+ title: 'Broker audit request received',
5645
+ detail: 'Your free broker lead-flow audit request is captured. The next step is the 1-page PDF with the specific lead leaks.',
5646
+ leadId: lead.leadId,
5647
+ }),
5648
+ journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}
5649
+ );
5650
+ return;
5651
+ }
5652
+
5653
+ sendJson(res, 201, {
5654
+ ok: true,
5655
+ leadId: lead.leadId,
5656
+ status: lead.status,
5657
+ offer: lead.offer,
5658
+ nextStep: 'deliver_1_page_pdf',
5659
+ }, {
5660
+ ...getPublicBillingHeaders(lead.attribution.traceId),
5661
+ ...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
5662
+ });
5663
+ } catch (err) {
5664
+ appendBestEffortTelemetry(FEEDBACK_DIR, {
5665
+ eventType: 'broker_audit_lead_failed',
5666
+ clientType: 'web',
5667
+ traceId,
5668
+ acquisitionId: journeyState.acquisitionId,
5669
+ visitorId: journeyState.visitorId,
5670
+ sessionId: journeyState.sessionId,
5671
+ source: referrerAttribution.source || 'website',
5672
+ utmSource: referrerAttribution.utmSource || 'website',
5673
+ utmMedium: referrerAttribution.utmMedium || 'broker_audit',
5674
+ utmCampaign: referrerAttribution.utmCampaign || 'broker_audit_free',
5675
+ ctaId: 'broker_audit_form',
5676
+ ctaPlacement: 'broker_audit',
5677
+ page: referrerAttribution.page || '/broker-audit',
5678
+ landingPath: referrerAttribution.landingPath || '/broker-audit',
5679
+ referrerHost: referrerAttribution.referrerHost,
5680
+ referrer: referrerAttribution.referrer,
5681
+ failureCode: err?.message ? err.message : 'broker_audit_lead_failed',
5682
+ httpStatus: err && err.statusCode ? err.statusCode : null,
5683
+ }, req.headers, 'broker_audit_lead_failed');
5684
+ if (isFormSubmission && !wantsJson(req, parsed)) {
5685
+ sendHtml(
5686
+ res,
5687
+ err.statusCode || 500,
5688
+ renderBrokerAuditIntakeResultPage(hostedConfig, {
5689
+ title: 'Broker audit request failed',
5690
+ detail: err.message || 'Unable to capture broker audit request.',
5691
+ }),
5692
+ journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}
5693
+ );
5694
+ return;
5695
+ }
5696
+ sendProblem(res, {
5697
+ type: !err.statusCode || err.statusCode >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.BAD_REQUEST,
5698
+ title: !err.statusCode || err.statusCode >= 500 ? 'Internal Server Error' : 'Request Error',
5699
+ status: err.statusCode || 500,
5700
+ detail: err.message || 'Unable to capture broker audit request.',
5701
+ }, getPublicBillingHeaders(traceId));
5702
+ }
5703
+ return;
5704
+ }
5705
+
5364
5706
  if (req.method === 'POST' && pathname === '/v1/intake/workflow-sprint') {
5365
5707
  const { FEEDBACK_DIR } = getFeedbackPaths();
5366
5708
  const traceId = createTraceId('sprint_intake');
@@ -5481,7 +5823,7 @@ async function addContext(){
5481
5823
  landingPath: referrerAttribution.landingPath || '/',
5482
5824
  referrerHost: referrerAttribution.referrerHost,
5483
5825
  referrer: referrerAttribution.referrer,
5484
- failureCode: err && err.message ? err.message : 'workflow_sprint_lead_failed',
5826
+ failureCode: err?.message ? err.message : 'workflow_sprint_lead_failed',
5485
5827
  httpStatus: err && err.statusCode ? err.statusCode : null,
5486
5828
  }, req.headers, 'workflow_sprint_lead_failed');
5487
5829
  if (isFormSubmission && !wantsJson(req, parsed)) {
@@ -5584,15 +5926,15 @@ async function addContext(){
5584
5926
  // surface that was missing: thumbgate.ai had no /case-studies, so visitors
5585
5927
  // landed on CLI install commands without seeing whether anyone actually
5586
5928
  // got value. First entry is the Aiventyx Teams listing integration: real
5587
- // third-party CTR signal (5/8 clicks before the /go/teams fix, end-to-end
5588
- // verified after).
5929
+ // third-party CTR signal (5/8 clicks before the /go/teams fix, now routed
5930
+ // through team intake so scope happens before checkout).
5589
5931
  if (isGetLikeRequest && pathname === '/case-studies') {
5590
5932
  sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Case Studies — ThumbGate</title><meta name="description" content="Real integrations of ThumbGate's pre-action checks for AI coding agents. Proof, not promises."><style>body{font-family:system-ui,-apple-system,sans-serif;max-width:780px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}h1{font-size:32px;margin:0 0 8px}.lede{color:#6b7280;font-size:18px;margin:0 0 32px}article{border:1px solid #e5e7eb;border-radius:12px;padding:24px;margin-bottom:24px;background:#fff}article h2{margin:0 0 4px;font-size:22px}.meta{color:#6b7280;font-size:13px;margin-bottom:16px}h3{font-size:15px;margin:20px 0 8px;color:#374151;text-transform:uppercase;letter-spacing:0.5px}.metric{display:inline-block;background:#0f172a;color:#fff;padding:4px 10px;border-radius:6px;font-weight:600;font-size:14px;margin:0 4px 4px 0}p{margin:8px 0}a{color:#0066cc}code{background:#f3f4f6;padding:1px 6px;border-radius:4px;font-size:13.5px}footer{margin-top:48px;padding-top:24px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
5591
5933
  <h1>Case Studies</h1>
5592
5934
  <p class="lede">Real integrations. No fabricated logos, no aspirational numbers — every claim below is reproducible.</p>
5593
5935
 
5594
5936
  <article>
5595
- <h2>Aiventyx marketplace — Teams listing CTR recovery</h2>
5937
+ <h2>Aiventyx marketplace — Teams listing intake recovery</h2>
5596
5938
  <p class="meta">Integration partner: <a href="https://www.aiventyx.com">Aiventyx</a> · Reported by: Qaiser Mehdi · Verified: 2026-05-13</p>
5597
5939
 
5598
5940
  <h3>The problem</h3>
@@ -5601,18 +5943,17 @@ async function addContext(){
5601
5943
  <p>The <code>/go/teams</code> slug wasn't registered in our redirector — a 404 was eating every paid-intent click from their strongest external surface.</p>
5602
5944
 
5603
5945
  <h3>The fix</h3>
5604
- <p>Added <code>teams</code> to <code>TRACKED_LINK_TARGETS</code>: HTTP 302 redirect to <code>/checkout/pro?plan_id=team&seat_count=3&billing_cycle=monthly</code> the 3-seat $147/mo self-serve Stripe Team checkout. Caller-supplied UTMs flow through to Stripe metadata end-to-end.</p>
5946
+ <p>Added <code>teams</code> to <code>TRACKED_LINK_TARGETS</code> and now routes it to <code>/?plan_id=team#workflow-sprint-intake</code>. Caller-supplied UTMs flow through to the intake path so the workflow, owner, and proof boundary are explicit before any Team checkout.</p>
5605
5947
 
5606
5948
  <h3>The verification</h3>
5607
5949
  <p>Qaiser's own incognito test, May 13 6:04 AM (full email on record):</p>
5608
5950
  <p><code>https://thumbgate.ai/go/teams?utm_source=aiventyx</code><br>
5609
- → 302 to Stripe checkout<br>
5610
- "Subscribe to ThumbGate Team" page loads<br>
5611
- $147/mo, 3-seat Team plan confirmed<br>
5612
- → Aiventyx UTMs intact in URL</p>
5951
+ → 302 to the Team workflow intake<br>
5952
+ pricing source, campaign, and plan metadata preserved<br>
5953
+ buyer sees the scope-first path before any subscription decision</p>
5613
5954
 
5614
5955
  <h3>What this proves</h3>
5615
- <p>End-to-end attribution from a third-party marketplace through ThumbGate's redirector into Stripe checkout, with the caller's UTM chain preserved. Two regression tests pin the redirect contract so it can't silently break.</p>
5956
+ <p>End-to-end attribution from a third-party marketplace through ThumbGate's redirector into the Team intake path, with the caller's UTM chain preserved. Regression tests pin the redirect contract so it can't silently break or regress into a blind Team checkout.</p>
5616
5957
 
5617
5958
  <p><a href="/go/teams?utm_source=case-study">Try the live redirect →</a></p>
5618
5959
  </article>
@@ -5634,112 +5975,26 @@ async function addContext(){
5634
5975
  // Sprint (proof-pack, sales-led) → Pro (self-serve recurring) → Team
5635
5976
  // (after qualification). Every paid CTA across the site should funnel
5636
5977
  // here OR directly into Stripe checkout — never a different price.
5637
- if (isGetLikeRequest && pathname === '/pricing') {
5638
- sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pricing — ThumbGate</title><meta name="description" content="ThumbGate pricing: free CLI, $19/mo Pro, $49/seat Team, $499 Sprint Diagnostic, $1500 full Workflow Hardening Sprint. Single source of truth across the site."><style>body{font-family:system-ui,-apple-system,sans-serif;max-width:980px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}h1{font-size:36px;margin:0 0 8px;text-align:center}.lede{color:#6b7280;font-size:18px;margin:0 0 32px;text-align:center}.grid{display:grid;grid-template-columns:1fr;gap:20px;margin-bottom:24px}@media(min-width:720px){.grid{grid-template-columns:repeat(2,1fr)}.hero{grid-column:1/-1}}.card{border:1px solid #e5e7eb;border-radius:12px;padding:24px;background:#fff;display:flex;flex-direction:column}.hero{border:2px solid #0f172a;background:linear-gradient(135deg,#fef3c7,#fff)}.tag{display:inline-block;background:#0f172a;color:#fff;padding:3px 10px;border-radius:6px;font-size:12px;font-weight:600;letter-spacing:0.5px;margin-bottom:12px}.tag-free{background:#10b981}.tag-pro{background:#3b82f6}.tag-team{background:#8b5cf6}.tag-diag{background:#0f172a}.tag-sprint{background:#b91c1c}h2{margin:0 0 4px;font-size:22px}.price{font-size:30px;font-weight:700;margin:8px 0 12px}.price small{font-size:14px;color:#6b7280;font-weight:400}.tagline{color:#374151;margin:0 0 16px;font-size:15px}ul{margin:0 0 20px;padding-left:18px}li{margin:6px 0;font-size:14px}.cta{display:inline-block;background:#0f172a;color:#fff;padding:12px 24px;border-radius:8px;font-weight:600;text-decoration:none;text-align:center;margin-top:auto}.cta-secondary{background:#fff;color:#0f172a;border:1px solid #0f172a}.cta-free{background:#10b981}.micro{border-top:1px solid #e5e7eb;padding-top:24px;margin-top:16px}.micro-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:16px 0}.micro-card{border:1px solid #e5e7eb;border-radius:8px;padding:14px;text-align:center;background:#fafafa}.micro-card .mp{font-size:20px;font-weight:700;color:#0f172a}.micro-card .ml{font-size:13px;color:#6b7280;margin:4px 0 8px}.micro-card a{color:#0066cc;font-size:13px;text-decoration:none}footer{margin-top:32px;padding-top:24px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px;text-align:center}footer a{color:#0066cc}</style></head><body>
5639
- <h1>Pricing</h1>
5640
- <p class="lede">Six paths to ThumbGate. Pick by what you need: an install, a subscription, a team rollout, or a stakeholder-visible artifact.</p>
5641
-
5642
- <div class="grid">
5643
-
5644
- <div class="card hero">
5645
- <span class="tag tag-sprint">Sprint — Full engagement</span>
5646
- <h2>Workflow Hardening Sprint</h2>
5647
- <div class="price">$1,500 <small>· one-time</small></div>
5648
- <p class="tagline">The full hardening engagement for a single AI-agent workflow. Built on top of the Sprint Diagnostic — we apply the diagnostic findings into a shipped Pre-Action Check pack, deploy hooks into your repo, and run the rollout review. Two weeks calendar-time, single fixed price.</p>
5649
- <ul>
5650
- <li>Diagnostic + applied rules + deployed PreToolUse hooks</li>
5651
- <li>Best path if you need the workflow actually fixed, not just diagnosed</li>
5652
- <li>Refund if we can't extract or apply a rule</li>
5653
- </ul>
5654
- <a class="cta" href="mailto:igor.ganapolsky@gmail.com?subject=ThumbGate%20Workflow%20Hardening%20Sprint%20-%20Intake&body=Stack%20(Claude%20Code%2FCursor%2Fother)%3A%0AOne%20repeated%20agent%20failure%20you%20want%20to%20kill%3A%0ATimeline%3A%0A">Email to start the full sprint →</a>
5655
- </div>
5656
-
5657
- <div class="card">
5658
- <span class="tag tag-diag">Diagnostic — Proof first</span>
5659
- <h2>Sprint Diagnostic</h2>
5660
- <div class="price">$499 <small>· one-time</small></div>
5661
- <p class="tagline">Two-day diagnostic on one workflow. Top-5 prevention rules ranked by impact, scoped Pre-Action Check pack delivered as a PR, 60-minute findings review. The lightweight on-ramp to the full sprint.</p>
5662
- <ul>
5663
- <li>Best if you need a stakeholder-visible artifact (PR, doc, briefing)</li>
5664
- <li>Two days fixed scope — no scope creep</li>
5665
- <li>Refund if we can't extract a rule from your failure trace</li>
5666
- </ul>
5667
- <a class="cta" href="https://buy.stripe.com/28E00j3Uge1E2dzgWL3sI2J">Pay $499 diagnostic →</a>
5668
- </div>
5669
-
5670
- <div class="card">
5671
- <span class="tag tag-free">Free — Forever</span>
5672
- <h2>ThumbGate CLI</h2>
5673
- <div class="price">$0</div>
5674
- <p class="tagline">MIT-licensed CLI + local PreToolUse hook. Unlimited captures, 5 active prevention rules, local lesson DB. Works with Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode.</p>
5675
- <ul>
5676
- <li>30-second install: <code>npx thumbgate init</code></li>
5677
- <li>No account, no signup, no data leaves your machine</li>
5678
- <li>Hit 5 active rules → upgrade to Pro for unlimited</li>
5679
- </ul>
5680
- <a class="cta cta-free" href="/go/install?utm_source=pricing">Install free CLI →</a>
5681
- </div>
5682
-
5683
- <div class="card">
5684
- <span class="tag tag-pro">Pro — Self-serve recurring</span>
5685
- <h2>ThumbGate Pro</h2>
5686
- <div class="price">$19 <small>/ month</small> · $149 / year</div>
5687
- <p class="tagline">For developers running multiple AI agents who hit the 5-rule wall. Unlimited prevention rules, local dashboard, DPO export for offline preference fine-tuning, lesson search across sessions.</p>
5688
- <ul>
5689
- <li>Unlimited active prevention rules</li>
5690
- <li>Local dashboard + lesson recall</li>
5691
- <li>7-day refund window. Cancel anytime.</li>
5692
- </ul>
5693
- <a class="cta cta-secondary" href="/go/pro?utm_source=pricing">Start Pro →</a>
5694
- </div>
5695
-
5696
- <div class="card">
5697
- <span class="tag tag-team">Team — After qualification</span>
5698
- <h2>ThumbGate Team</h2>
5699
- <div class="price">$49 <small>/ seat / month</small> · 3-seat min ($147/mo)</div>
5700
- <p class="tagline">For engineering teams with shared AI-agent workflows. Shared lesson DB so one engineer's save protects the whole team, org-level policy rollout, audit-ready evidence.</p>
5701
- <ul>
5702
- <li>Shared prevention-rule policy across seats</li>
5703
- <li>Self-serve checkout — no sales call required</li>
5704
- <li>Most teams start with a Sprint first, then scale to seats</li>
5705
- </ul>
5706
- <a class="cta cta-secondary" href="/go/teams?utm_source=pricing">Start Team →</a>
5707
- </div>
5708
-
5709
- </div>
5710
-
5711
- <div class="micro">
5712
- <h2 style="text-align:center;font-size:20px;margin:0 0 4px;">Micro-purchases — pay for one piece</h2>
5713
- <p style="text-align:center;color:#6b7280;font-size:14px;margin:0 0 8px;">For evaluators who want to validate one specific surface before subscribing.</p>
5714
- <div class="micro-grid">
5715
- <div class="micro-card">
5716
- <div class="mp">$1</div>
5717
- <div class="ml">First Failure Rule</div>
5718
- <a href="https://buy.stripe.com/fZu28rfCY6zcbO99uj3sI2G">Pay $1 →</a>
5719
- </div>
5720
- <div class="micro-card">
5721
- <div class="mp">$19</div>
5722
- <div class="ml">AI Agent Failure Quick Read</div>
5723
- <a href="https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H">Pay $19 →</a>
5724
- </div>
5725
- <div class="micro-card">
5726
- <div class="mp">$99</div>
5727
- <div class="ml">Workflow Teardown</div>
5728
- <a href="https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I">Pay $99 →</a>
5729
- </div>
5730
- </div>
5731
- </div>
5732
-
5733
- <footer>
5734
- <p>One source of truth for ThumbGate pricing. Numbers here override anything stale elsewhere on the site.</p>
5735
- <p><a href="/">Home</a> · <a href="/case-studies">Case Studies</a> · <a href="/support">Support</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></p>
5736
- </footer>
5737
- </body></html>`, {}, {
5738
- headOnly: isHeadRequest,
5739
- });
5978
+ if (isGetLikeRequest && (pathname === '/pricing' || pathname === '/pricing.html')) {
5979
+ try {
5980
+ servePublicMarketingPage({
5981
+ req,
5982
+ res,
5983
+ parsed,
5984
+ hostedConfig,
5985
+ isHeadRequest,
5986
+ renderHtml: loadPricingPageHtml,
5987
+ extraTelemetry: {
5988
+ pageType: 'pricing',
5989
+ },
5990
+ });
5991
+ } catch (err) {
5992
+ sendText(res, 500, err.message || 'Pricing page unavailable');
5993
+ }
5740
5994
  return;
5741
5995
  }
5742
5996
 
5997
+
5743
5998
  // Public support / contact page — required for Stripe Business → Public
5744
5999
  // details "Customer support URL" field. Single source of truth for how
5745
6000
  // customers reach us (email, GitHub issues, status page).
@@ -6423,6 +6678,11 @@ async function addContext(){
6423
6678
  }
6424
6679
 
6425
6680
  if (req.method === 'GET' && pathname === '/v1/lessons/search') {
6681
+ const lessonLimit = checkLimit('search_lessons');
6682
+ if (!lessonLimit.allowed) {
6683
+ sendJson(res, 429, { error: 'Free tier: lesson search requires Pro. Upgrade at /pricing', code: 'PRO_REQUIRED' });
6684
+ return;
6685
+ }
6426
6686
  const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
6427
6687
  const limit = Number(parsed.searchParams.get('limit') || 10);
6428
6688
  const category = parsed.searchParams.get('category') || '';
@@ -6442,6 +6702,11 @@ async function addContext(){
6442
6702
  }
6443
6703
 
6444
6704
  if (req.method === 'GET' && pathname === '/v1/search') {
6705
+ const searchLimit = checkLimit('search_thumbgate');
6706
+ if (!searchLimit.allowed) {
6707
+ sendJson(res, 429, { error: 'Free tier: search requires Pro. Upgrade at /pricing', code: 'PRO_REQUIRED' });
6708
+ return;
6709
+ }
6445
6710
  const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
6446
6711
  const limit = Number(parsed.searchParams.get('limit') || 10);
6447
6712
  const source = parsed.searchParams.get('source') || 'all';
@@ -6462,6 +6727,11 @@ async function addContext(){
6462
6727
  }
6463
6728
 
6464
6729
  if (req.method === 'POST' && pathname === '/v1/search') {
6730
+ const searchLimit = checkLimit('search_thumbgate');
6731
+ if (!searchLimit.allowed) {
6732
+ sendJson(res, 429, { error: 'Free tier: search requires Pro. Upgrade at /pricing', code: 'PRO_REQUIRED' });
6733
+ return;
6734
+ }
6465
6735
  const body = await parseJsonBody(req);
6466
6736
  let results;
6467
6737
  try {
@@ -6625,6 +6895,11 @@ async function addContext(){
6625
6895
  }
6626
6896
 
6627
6897
  if (req.method === 'POST' && pathname === '/v1/dpo/export') {
6898
+ const dpoLimit = checkLimit('export_dpo');
6899
+ if (!dpoLimit.allowed) {
6900
+ sendJson(res, 429, { error: 'Free tier: DPO export requires Pro. Upgrade at /pricing', code: 'PRO_REQUIRED' });
6901
+ return;
6902
+ }
6628
6903
  const body = await parseJsonBody(req);
6629
6904
  const paths = resolveDpoExportPaths(body, {
6630
6905
  safeDataDir: requestSafeDataDir,
@@ -7107,7 +7382,7 @@ async function addContext(){
7107
7382
  type: err && err.statusCode === 404 ? PROBLEM_TYPES.NOT_FOUND : PROBLEM_TYPES.BAD_REQUEST,
7108
7383
  title: err && err.statusCode === 404 ? 'Lead Not Found' : 'Request Error',
7109
7384
  status: err && err.statusCode ? err.statusCode : 400,
7110
- detail: err && err.message ? err.message : 'Unable to advance workflow sprint lead.',
7385
+ detail: err?.message ? err.message : 'Unable to advance workflow sprint lead.',
7111
7386
  });
7112
7387
  }
7113
7388
  return;
@@ -7256,7 +7531,7 @@ async function addContext(){
7256
7531
  type: PROBLEM_TYPES.INVALID_REQUEST,
7257
7532
  title: 'Invalid render spec request',
7258
7533
  status: 400,
7259
- detail: err && err.message ? err.message : 'Unable to build dashboard render spec.',
7534
+ detail: err?.message ? err.message : 'Unable to build dashboard render spec.',
7260
7535
  });
7261
7536
  }
7262
7537
  return;