thumbgate 1.20.0 → 1.21.1
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 +28 -8
- 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 +119 -2
- 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 +74 -60
- package/public/agent-manager.html +139 -0
- package/public/compare.html +1 -1
- package/public/dashboard.html +3 -3
- package/public/guide.html +23 -0
- package/public/index.html +79 -133
- package/public/learn.html +16 -0
- package/public/lessons.html +22 -0
- package/public/numbers.html +2 -2
- package/public/pricing.html +345 -0
- package/scripts/auto-promote-gates.js +7 -6
- package/scripts/billing.js +64 -0
- 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 +36 -1
- package/scripts/tool-registry.js +28 -0
- package/scripts/verify-marketing-pages-deployed.js +195 -0
- package/scripts/workflow-sentinel.js +6 -1
- package/src/api/server.js +514 -142
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
|
|
|
@@ -1503,7 +1498,7 @@ function renderCheckoutIntentPage({
|
|
|
1503
1498
|
}) {
|
|
1504
1499
|
const safeConfirmHref = escapeHtmlAttribute(confirmHref);
|
|
1505
1500
|
const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
|
|
1506
|
-
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>
|
|
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>`;
|
|
1507
1502
|
}
|
|
1508
1503
|
|
|
1509
1504
|
function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
|
|
@@ -1812,7 +1807,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
|
|
|
1812
1807
|
metadata: {
|
|
1813
1808
|
context,
|
|
1814
1809
|
eventType: payload && (payload.eventType || payload.event) ? payload.eventType || payload.event : 'unknown',
|
|
1815
|
-
error: err
|
|
1810
|
+
error: err?.message ? err.message : 'unknown_error',
|
|
1816
1811
|
},
|
|
1817
1812
|
diagnosis: {
|
|
1818
1813
|
diagnosed: true,
|
|
@@ -1822,7 +1817,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
|
|
|
1822
1817
|
constraintId: 'telemetry:emit',
|
|
1823
1818
|
message: 'Server-side telemetry write failed.',
|
|
1824
1819
|
}],
|
|
1825
|
-
evidence: [err
|
|
1820
|
+
evidence: [err?.message ? err.message : 'unknown_error'],
|
|
1826
1821
|
},
|
|
1827
1822
|
});
|
|
1828
1823
|
} catch (_) {}
|
|
@@ -1968,6 +1963,10 @@ function loadProPageHtml(runtimeConfig, pageContext = {}) {
|
|
|
1968
1963
|
return loadPublicMarketingTemplateHtml(PRO_PAGE_PATH, runtimeConfig, pageContext);
|
|
1969
1964
|
}
|
|
1970
1965
|
|
|
1966
|
+
function loadPricingPageHtml(runtimeConfig, pageContext = {}) {
|
|
1967
|
+
return loadPublicMarketingTemplateHtml(PRICING_PAGE_PATH, runtimeConfig, pageContext);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1971
1970
|
function readOptionalPublicTemplate(filePath) {
|
|
1972
1971
|
try {
|
|
1973
1972
|
return fs.readFileSync(filePath, 'utf-8');
|
|
@@ -2502,6 +2501,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
2502
2501
|
const entries = [
|
|
2503
2502
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
2504
2503
|
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
2504
|
+
{ path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
|
|
2505
2505
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
2506
2506
|
{ path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
|
|
2507
2507
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
@@ -2980,13 +2980,13 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2980
2980
|
curlBlock.textContent = body.nextSteps && body.nextSteps.curl ? body.nextSteps.curl : 'curl snippet unavailable.';
|
|
2981
2981
|
} catch (err) {
|
|
2982
2982
|
sendTelemetryOnce('checkout_session_lookup_failed', {
|
|
2983
|
-
failureCode: err
|
|
2983
|
+
failureCode: err?.message ? err.message : 'checkout_session_lookup_failed',
|
|
2984
2984
|
});
|
|
2985
2985
|
statusEl.textContent = 'Provisioning lookup failed.';
|
|
2986
2986
|
summaryEl.textContent = traceId
|
|
2987
2987
|
? 'You can retry this page. If it keeps failing, inspect the hosted API logs with trace ' + traceId + '.'
|
|
2988
2988
|
: 'You can retry this page. If it keeps failing, inspect the hosted API logs.';
|
|
2989
|
-
keyBlock.textContent = err
|
|
2989
|
+
keyBlock.textContent = err?.message ? err.message : 'Unknown error';
|
|
2990
2990
|
}
|
|
2991
2991
|
}
|
|
2992
2992
|
|
|
@@ -3352,6 +3352,39 @@ function renderWorkflowSprintIntakeResultPage(runtimeConfig, { title, detail, le
|
|
|
3352
3352
|
</html>`;
|
|
3353
3353
|
}
|
|
3354
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
|
+
|
|
3355
3388
|
function readBodyBuffer(req, maxBytes = 1024 * 1024) {
|
|
3356
3389
|
return new Promise((resolve, reject) => {
|
|
3357
3390
|
let total = 0;
|
|
@@ -3396,6 +3429,72 @@ async function parseFormBody(req, maxBytes = 1024 * 1024) {
|
|
|
3396
3429
|
return Object.fromEntries(params.entries());
|
|
3397
3430
|
}
|
|
3398
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
|
+
|
|
3399
3498
|
function parseOptionalObject(input, name) {
|
|
3400
3499
|
if (input == null) return {};
|
|
3401
3500
|
if (typeof input === 'object' && !Array.isArray(input)) return input;
|
|
@@ -3855,7 +3954,7 @@ function createApiServer() {
|
|
|
3855
3954
|
}
|
|
3856
3955
|
})
|
|
3857
3956
|
.catch((err) => {
|
|
3858
|
-
console.warn('[newsletter] welcome email threw:', email, err
|
|
3957
|
+
console.warn('[newsletter] welcome email threw:', email, err?.message);
|
|
3859
3958
|
});
|
|
3860
3959
|
}
|
|
3861
3960
|
const journeyState = resolveJourneyState(req, parsed);
|
|
@@ -4333,6 +4432,29 @@ async function addContext(){
|
|
|
4333
4432
|
return;
|
|
4334
4433
|
}
|
|
4335
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
|
+
|
|
4336
4458
|
if (isGetLikeRequest && (pathname === '/numbers' || pathname === '/numbers.html')) {
|
|
4337
4459
|
// Route through servePublicMarketingPage so landing_page_view telemetry
|
|
4338
4460
|
// + funnel-events.jsonl `discovery/landing_view` get captured with UTM
|
|
@@ -4375,6 +4497,29 @@ async function addContext(){
|
|
|
4375
4497
|
return;
|
|
4376
4498
|
}
|
|
4377
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
|
+
|
|
4378
4523
|
if (isGetLikeRequest && pathname === '/learn/learn.css') {
|
|
4379
4524
|
try {
|
|
4380
4525
|
const cssPath = path.join(LEARN_DIR, 'learn.css');
|
|
@@ -4466,7 +4611,7 @@ async function addContext(){
|
|
|
4466
4611
|
version: pkg.version,
|
|
4467
4612
|
status: 'ok',
|
|
4468
4613
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
4469
|
-
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'],
|
|
4470
4615
|
}, {}, {
|
|
4471
4616
|
headOnly: isHeadRequest,
|
|
4472
4617
|
});
|
|
@@ -4510,9 +4655,22 @@ async function addContext(){
|
|
|
4510
4655
|
|
|
4511
4656
|
const botClassification = classifyRequester(req.headers);
|
|
4512
4657
|
const confirmParam = parsed?.searchParams?.get('confirm') ?? null;
|
|
4513
|
-
const
|
|
4514
|
-
|
|
4515
|
-
|
|
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);
|
|
4516
4674
|
// Plausible funnel event #1 of 3: page view. Fired before interstitial
|
|
4517
4675
|
// deflection so we get the full top-of-funnel count, with isBot as a
|
|
4518
4676
|
// prop so the dashboard can filter human vs. crawler traffic. Fire-and-forget.
|
|
@@ -4543,7 +4701,7 @@ async function addContext(){
|
|
|
4543
4701
|
// and must click "Pay $19/mo with Stripe →" (which sets confirm=1) to
|
|
4544
4702
|
// trigger the Stripe-session creation + redirect. Counter-risk: one
|
|
4545
4703
|
// extra click on the human path. Mitigated because the interstitial
|
|
4546
|
-
// also serves as a value-preview ("
|
|
4704
|
+
// also serves as a value-preview ("5,200+ npm installs/mo, MIT open source,
|
|
4547
4705
|
// cancel anytime"), which typically lifts conversion more than the
|
|
4548
4706
|
// click-friction costs.
|
|
4549
4707
|
if (!isConfirmedCheckout) {
|
|
@@ -4783,7 +4941,7 @@ async function addContext(){
|
|
|
4783
4941
|
seatCount: analyticsMetadata.seatCount,
|
|
4784
4942
|
referrer: analyticsMetadata.referrer,
|
|
4785
4943
|
referrerHost: analyticsMetadata.referrerHost,
|
|
4786
|
-
failureCode: err
|
|
4944
|
+
failureCode: err?.message ? err.message : 'checkout_bootstrap_failed',
|
|
4787
4945
|
httpStatus: err && err.statusCode ? err.statusCode : null,
|
|
4788
4946
|
}, req.headers, 'checkout_api_failed');
|
|
4789
4947
|
res.writeHead(302, {
|
|
@@ -4841,6 +4999,28 @@ async function addContext(){
|
|
|
4841
4999
|
return;
|
|
4842
5000
|
}
|
|
4843
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
|
+
|
|
4844
5024
|
if (isGetLikeRequest && pathname === '/.well-known/mcp.json') {
|
|
4845
5025
|
sendJson(res, 200, getMcpDiscoveryManifest(hostedConfig), {}, {
|
|
4846
5026
|
headOnly: isHeadRequest,
|
|
@@ -5050,7 +5230,7 @@ async function addContext(){
|
|
|
5050
5230
|
path: pathname,
|
|
5051
5231
|
method: req.method,
|
|
5052
5232
|
reason: err && err.statusCode && err.statusCode < 500 ? 'invalid_payload' : 'write_failed',
|
|
5053
|
-
error: err
|
|
5233
|
+
error: err?.message ? err.message : 'unknown_error',
|
|
5054
5234
|
},
|
|
5055
5235
|
diagnosis: {
|
|
5056
5236
|
diagnosed: true,
|
|
@@ -5060,7 +5240,7 @@ async function addContext(){
|
|
|
5060
5240
|
constraintId: 'telemetry:ingest',
|
|
5061
5241
|
message: 'Telemetry ping could not be processed.',
|
|
5062
5242
|
}],
|
|
5063
|
-
evidence: [err
|
|
5243
|
+
evidence: [err?.message ? err.message : 'unknown_error'],
|
|
5064
5244
|
},
|
|
5065
5245
|
});
|
|
5066
5246
|
} catch (_) {
|
|
@@ -5259,11 +5439,270 @@ async function addContext(){
|
|
|
5259
5439
|
return;
|
|
5260
5440
|
}
|
|
5261
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
|
+
|
|
5262
5577
|
if (req.method === 'OPTIONS' && pathname === '/v1/intake/workflow-sprint') {
|
|
5263
5578
|
sendPublicBillingPreflight(res);
|
|
5264
5579
|
return;
|
|
5265
5580
|
}
|
|
5266
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
|
+
|
|
5267
5706
|
if (req.method === 'POST' && pathname === '/v1/intake/workflow-sprint') {
|
|
5268
5707
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
5269
5708
|
const traceId = createTraceId('sprint_intake');
|
|
@@ -5384,7 +5823,7 @@ async function addContext(){
|
|
|
5384
5823
|
landingPath: referrerAttribution.landingPath || '/',
|
|
5385
5824
|
referrerHost: referrerAttribution.referrerHost,
|
|
5386
5825
|
referrer: referrerAttribution.referrer,
|
|
5387
|
-
failureCode: err
|
|
5826
|
+
failureCode: err?.message ? err.message : 'workflow_sprint_lead_failed',
|
|
5388
5827
|
httpStatus: err && err.statusCode ? err.statusCode : null,
|
|
5389
5828
|
}, req.headers, 'workflow_sprint_lead_failed');
|
|
5390
5829
|
if (isFormSubmission && !wantsJson(req, parsed)) {
|
|
@@ -5487,15 +5926,15 @@ async function addContext(){
|
|
|
5487
5926
|
// surface that was missing: thumbgate.ai had no /case-studies, so visitors
|
|
5488
5927
|
// landed on CLI install commands without seeing whether anyone actually
|
|
5489
5928
|
// got value. First entry is the Aiventyx Teams listing integration: real
|
|
5490
|
-
// third-party CTR signal (5/8 clicks before the /go/teams fix,
|
|
5491
|
-
//
|
|
5929
|
+
// third-party CTR signal (5/8 clicks before the /go/teams fix, now routed
|
|
5930
|
+
// through team intake so scope happens before checkout).
|
|
5492
5931
|
if (isGetLikeRequest && pathname === '/case-studies') {
|
|
5493
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>
|
|
5494
5933
|
<h1>Case Studies</h1>
|
|
5495
5934
|
<p class="lede">Real integrations. No fabricated logos, no aspirational numbers — every claim below is reproducible.</p>
|
|
5496
5935
|
|
|
5497
5936
|
<article>
|
|
5498
|
-
<h2>Aiventyx marketplace — Teams listing
|
|
5937
|
+
<h2>Aiventyx marketplace — Teams listing intake recovery</h2>
|
|
5499
5938
|
<p class="meta">Integration partner: <a href="https://www.aiventyx.com">Aiventyx</a> · Reported by: Qaiser Mehdi · Verified: 2026-05-13</p>
|
|
5500
5939
|
|
|
5501
5940
|
<h3>The problem</h3>
|
|
@@ -5504,18 +5943,17 @@ async function addContext(){
|
|
|
5504
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>
|
|
5505
5944
|
|
|
5506
5945
|
<h3>The fix</h3>
|
|
5507
|
-
<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>
|
|
5508
5947
|
|
|
5509
5948
|
<h3>The verification</h3>
|
|
5510
5949
|
<p>Qaiser's own incognito test, May 13 6:04 AM (full email on record):</p>
|
|
5511
5950
|
<p><code>https://thumbgate.ai/go/teams?utm_source=aiventyx</code><br>
|
|
5512
|
-
→ 302 to
|
|
5513
|
-
→
|
|
5514
|
-
→
|
|
5515
|
-
→ 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>
|
|
5516
5954
|
|
|
5517
5955
|
<h3>What this proves</h3>
|
|
5518
|
-
<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>
|
|
5519
5957
|
|
|
5520
5958
|
<p><a href="/go/teams?utm_source=case-study">Try the live redirect →</a></p>
|
|
5521
5959
|
</article>
|
|
@@ -5537,112 +5975,26 @@ async function addContext(){
|
|
|
5537
5975
|
// Sprint (proof-pack, sales-led) → Pro (self-serve recurring) → Team
|
|
5538
5976
|
// (after qualification). Every paid CTA across the site should funnel
|
|
5539
5977
|
// here OR directly into Stripe checkout — never a different price.
|
|
5540
|
-
if (isGetLikeRequest && pathname === '/pricing') {
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
</ul>
|
|
5557
|
-
<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>
|
|
5558
|
-
</div>
|
|
5559
|
-
|
|
5560
|
-
<div class="card">
|
|
5561
|
-
<span class="tag tag-diag">Diagnostic — Proof first</span>
|
|
5562
|
-
<h2>Sprint Diagnostic</h2>
|
|
5563
|
-
<div class="price">$499 <small>· one-time</small></div>
|
|
5564
|
-
<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>
|
|
5565
|
-
<ul>
|
|
5566
|
-
<li>Best if you need a stakeholder-visible artifact (PR, doc, briefing)</li>
|
|
5567
|
-
<li>Two days fixed scope — no scope creep</li>
|
|
5568
|
-
<li>Refund if we can't extract a rule from your failure trace</li>
|
|
5569
|
-
</ul>
|
|
5570
|
-
<a class="cta" href="https://buy.stripe.com/28E00j3Uge1E2dzgWL3sI2J">Pay $499 diagnostic →</a>
|
|
5571
|
-
</div>
|
|
5572
|
-
|
|
5573
|
-
<div class="card">
|
|
5574
|
-
<span class="tag tag-free">Free — Forever</span>
|
|
5575
|
-
<h2>ThumbGate CLI</h2>
|
|
5576
|
-
<div class="price">$0</div>
|
|
5577
|
-
<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>
|
|
5578
|
-
<ul>
|
|
5579
|
-
<li>30-second install: <code>npx thumbgate init</code></li>
|
|
5580
|
-
<li>No account, no signup, no data leaves your machine</li>
|
|
5581
|
-
<li>Hit 5 active rules → upgrade to Pro for unlimited</li>
|
|
5582
|
-
</ul>
|
|
5583
|
-
<a class="cta cta-free" href="/go/install?utm_source=pricing">Install free CLI →</a>
|
|
5584
|
-
</div>
|
|
5585
|
-
|
|
5586
|
-
<div class="card">
|
|
5587
|
-
<span class="tag tag-pro">Pro — Self-serve recurring</span>
|
|
5588
|
-
<h2>ThumbGate Pro</h2>
|
|
5589
|
-
<div class="price">$19 <small>/ month</small> · $149 / year</div>
|
|
5590
|
-
<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>
|
|
5591
|
-
<ul>
|
|
5592
|
-
<li>Unlimited active prevention rules</li>
|
|
5593
|
-
<li>Local dashboard + lesson recall</li>
|
|
5594
|
-
<li>7-day refund window. Cancel anytime.</li>
|
|
5595
|
-
</ul>
|
|
5596
|
-
<a class="cta cta-secondary" href="/go/pro?utm_source=pricing">Start Pro →</a>
|
|
5597
|
-
</div>
|
|
5598
|
-
|
|
5599
|
-
<div class="card">
|
|
5600
|
-
<span class="tag tag-team">Team — After qualification</span>
|
|
5601
|
-
<h2>ThumbGate Team</h2>
|
|
5602
|
-
<div class="price">$49 <small>/ seat / month</small> · 3-seat min ($147/mo)</div>
|
|
5603
|
-
<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>
|
|
5604
|
-
<ul>
|
|
5605
|
-
<li>Shared prevention-rule policy across seats</li>
|
|
5606
|
-
<li>Self-serve checkout — no sales call required</li>
|
|
5607
|
-
<li>Most teams start with a Sprint first, then scale to seats</li>
|
|
5608
|
-
</ul>
|
|
5609
|
-
<a class="cta cta-secondary" href="/go/teams?utm_source=pricing">Start Team →</a>
|
|
5610
|
-
</div>
|
|
5611
|
-
|
|
5612
|
-
</div>
|
|
5613
|
-
|
|
5614
|
-
<div class="micro">
|
|
5615
|
-
<h2 style="text-align:center;font-size:20px;margin:0 0 4px;">Micro-purchases — pay for one piece</h2>
|
|
5616
|
-
<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>
|
|
5617
|
-
<div class="micro-grid">
|
|
5618
|
-
<div class="micro-card">
|
|
5619
|
-
<div class="mp">$1</div>
|
|
5620
|
-
<div class="ml">First Failure Rule</div>
|
|
5621
|
-
<a href="https://buy.stripe.com/fZu28rfCY6zcbO99uj3sI2G">Pay $1 →</a>
|
|
5622
|
-
</div>
|
|
5623
|
-
<div class="micro-card">
|
|
5624
|
-
<div class="mp">$19</div>
|
|
5625
|
-
<div class="ml">AI Agent Failure Quick Read</div>
|
|
5626
|
-
<a href="https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H">Pay $19 →</a>
|
|
5627
|
-
</div>
|
|
5628
|
-
<div class="micro-card">
|
|
5629
|
-
<div class="mp">$99</div>
|
|
5630
|
-
<div class="ml">Workflow Teardown</div>
|
|
5631
|
-
<a href="https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I">Pay $99 →</a>
|
|
5632
|
-
</div>
|
|
5633
|
-
</div>
|
|
5634
|
-
</div>
|
|
5635
|
-
|
|
5636
|
-
<footer>
|
|
5637
|
-
<p>One source of truth for ThumbGate pricing. Numbers here override anything stale elsewhere on the site.</p>
|
|
5638
|
-
<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>
|
|
5639
|
-
</footer>
|
|
5640
|
-
</body></html>`, {}, {
|
|
5641
|
-
headOnly: isHeadRequest,
|
|
5642
|
-
});
|
|
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
|
+
}
|
|
5643
5994
|
return;
|
|
5644
5995
|
}
|
|
5645
5996
|
|
|
5997
|
+
|
|
5646
5998
|
// Public support / contact page — required for Stripe Business → Public
|
|
5647
5999
|
// details "Customer support URL" field. Single source of truth for how
|
|
5648
6000
|
// customers reach us (email, GitHub issues, status page).
|
|
@@ -6326,6 +6678,11 @@ async function addContext(){
|
|
|
6326
6678
|
}
|
|
6327
6679
|
|
|
6328
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
|
+
}
|
|
6329
6686
|
const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
|
|
6330
6687
|
const limit = Number(parsed.searchParams.get('limit') || 10);
|
|
6331
6688
|
const category = parsed.searchParams.get('category') || '';
|
|
@@ -6345,6 +6702,11 @@ async function addContext(){
|
|
|
6345
6702
|
}
|
|
6346
6703
|
|
|
6347
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
|
+
}
|
|
6348
6710
|
const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
|
|
6349
6711
|
const limit = Number(parsed.searchParams.get('limit') || 10);
|
|
6350
6712
|
const source = parsed.searchParams.get('source') || 'all';
|
|
@@ -6365,6 +6727,11 @@ async function addContext(){
|
|
|
6365
6727
|
}
|
|
6366
6728
|
|
|
6367
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
|
+
}
|
|
6368
6735
|
const body = await parseJsonBody(req);
|
|
6369
6736
|
let results;
|
|
6370
6737
|
try {
|
|
@@ -6528,6 +6895,11 @@ async function addContext(){
|
|
|
6528
6895
|
}
|
|
6529
6896
|
|
|
6530
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
|
+
}
|
|
6531
6903
|
const body = await parseJsonBody(req);
|
|
6532
6904
|
const paths = resolveDpoExportPaths(body, {
|
|
6533
6905
|
safeDataDir: requestSafeDataDir,
|
|
@@ -7010,7 +7382,7 @@ async function addContext(){
|
|
|
7010
7382
|
type: err && err.statusCode === 404 ? PROBLEM_TYPES.NOT_FOUND : PROBLEM_TYPES.BAD_REQUEST,
|
|
7011
7383
|
title: err && err.statusCode === 404 ? 'Lead Not Found' : 'Request Error',
|
|
7012
7384
|
status: err && err.statusCode ? err.statusCode : 400,
|
|
7013
|
-
detail: err
|
|
7385
|
+
detail: err?.message ? err.message : 'Unable to advance workflow sprint lead.',
|
|
7014
7386
|
});
|
|
7015
7387
|
}
|
|
7016
7388
|
return;
|
|
@@ -7159,7 +7531,7 @@ async function addContext(){
|
|
|
7159
7531
|
type: PROBLEM_TYPES.INVALID_REQUEST,
|
|
7160
7532
|
title: 'Invalid render spec request',
|
|
7161
7533
|
status: 400,
|
|
7162
|
-
detail: err
|
|
7534
|
+
detail: err?.message ? err.message : 'Unable to build dashboard render spec.',
|
|
7163
7535
|
});
|
|
7164
7536
|
}
|
|
7165
7537
|
return;
|