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/.claude-plugin/marketplace.json +40 -12
- package/.claude-plugin/plugin.json +15 -6
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +34 -14
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +15 -5
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +131 -3
- package/bin/postinstall.js +19 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/post-deploy-marketing-pages.json +46 -0
- package/package.json +75 -60
- package/public/agent-manager.html +139 -0
- package/public/compare.html +6 -0
- package/public/guide.html +23 -0
- package/public/index.html +100 -127
- package/public/learn.html +31 -0
- package/public/numbers.html +2 -2
- package/public/pricing.html +345 -0
- package/public/pro.html +3 -22
- package/scripts/auto-promote-gates.js +160 -13
- package/scripts/auto-wire-hooks.js +50 -29
- package/scripts/billing.js +64 -0
- package/scripts/cli-feedback.js +9 -1
- package/scripts/context-manager.js +42 -2
- package/scripts/feedback-loop.js +2 -1
- package/scripts/gates-engine.js +133 -7
- package/scripts/license.js +0 -1
- package/scripts/rate-limiter.js +47 -1
- package/scripts/tool-registry.js +28 -0
- package/scripts/verify-marketing-pages-deployed.js +195 -0
- package/src/api/server.js +514 -239
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-
|
|
417
|
-
//
|
|
418
|
-
//
|
|
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: '
|
|
421
|
+
path: '/#workflow-sprint-intake',
|
|
424
422
|
ctaId: 'go_teams',
|
|
425
423
|
ctaPlacement: 'link_router',
|
|
426
|
-
eventType: '
|
|
424
|
+
eventType: 'team_intake_started',
|
|
427
425
|
defaults: {
|
|
428
426
|
utm_source: 'website',
|
|
429
427
|
utm_medium: 'link_router',
|
|
430
|
-
utm_campaign: '
|
|
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
|
|
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
|
-
|
|
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 <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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4553
|
-
|
|
4554
|
-
|
|
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 ("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
5588
|
-
//
|
|
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
|
|
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
|
|
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
|
|
5610
|
-
→
|
|
5611
|
-
→
|
|
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
|
|
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
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
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
|
|
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
|
|
7534
|
+
detail: err?.message ? err.message : 'Unable to build dashboard render spec.',
|
|
7260
7535
|
});
|
|
7261
7536
|
}
|
|
7262
7537
|
return;
|